Skip to content
Back to skills

Vue Expert

ASecurity

Use when Vue 3.5+ Composition API. Script setup, reactive refs, computed, watchers, composables, Pinia, Vue Router 4, Nuxt 4. Use when building Vue/Nuxt applications.

  • 5 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 27, 2026
ai-agentsbashreactvuenodeapiperformance

Works with

  • api

Security analysis

A100/100

Scanned September 27, 2026

npx -y skills add Harmitx7/tribunal-kit --skill vue-expert --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Vue Expert?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Vue Expert
[![Security: A β€” Skills Directory](https://www.skillsdirectory.com/api/skills/harmitx7-vue-expert-tribunal-kit/badge)](https://www.skillsdirectory.com/skills/harmitx7-vue-expert-tribunal-kit)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: vue-expert
description: "Use when Vue 3.5+ Composition API. Script setup, reactive refs, computed, watchers, composables, Pinia, Vue Router 4, Nuxt 4. Use when building Vue/Nuxt applications."
version: 5.0.0
last-updated: 2026-09-13
skills:
  - baseline-ui
  - better-ui
  - 60fps-animation
tools: Read, Grep, Glob, Bash, Edit, Write
scripts-binding:
  - .agent/scripts/lint_runner.js
  - .agent/scripts/verify_all.js
---

# Vue 3.5+ & Nuxt 4 β€” Dense Reference

---

## πŸ› οΈ Technical Architecture & Reference Recipes

---

## Hallucination Traps (Read First)

- ❌ Options API (`data()`, `methods:`, `computed:`) β†’ βœ… `<script setup lang="ts">`
- ❌ `defineComponent()` with `<script setup>` β†’ βœ… redundant, skip it
- ❌ `defineModel` in Vue < 3.4 β†’ βœ… added in 3.4+
- ❌ `ref.value` in template β†’ βœ… auto-unwrapped in template (no `.value`)
- ❌ `reactive()` for primitives β†’ βœ… use `ref()` β€” `reactive()` breaks on reassign
- ❌ `watch(state.count, ...)` (primitive) β†’ βœ… `watch(() => state.count, ...)`
- ❌ `onBeforeMount` for data fetch β†’ βœ… use `await` directly in `<script setup>` + `<Suspense>`
- ❌ Pinia `this.$store` β†’ βœ… `useStore()` from `pinia`
- ❌ `useRoute()` / `useRouter()` outside setup β†’ βœ… only works inside `<script setup>` or composables

---

## `<script setup>` β€” The Only Way

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

// Props
const props = defineProps<{ title: string; count?: number }>();
// With defaults:
const props = withDefaults(defineProps<{ variant?: 'primary' | 'secondary' }>(), {
  variant: 'primary',
});

// Emits
const emit = defineEmits<{ update: [value: string]; delete: [id: number] }>();

// v-model (Vue 3.4+)
const modelValue = defineModel<string>(); // default model
const count = defineModel<number>('count'); // named model

// Expose to parent ref
defineExpose({ reset: () => {}, focus: () => {} });
</script>
```

---

## Reactivity

```ts
// ref β€” for primitives and objects (access via .value in JS, auto-unwrap in template)
const count = ref(0);
count.value++;

// reactive β€” for objects (loses reactivity on reassign/destructure)
const state = reactive({ name: 'Alice', age: 25 });
// ❌ const { name } = state; // loses reactivity
// βœ… const name = computed(() => state.name);

// computed β€” cached, re-runs only when deps change
const doubled = computed(() => count.value * 2);
const fullName = computed({
  get: () => `${first.value} ${last.value}`,
  set: v => {
    [first.value, last.value] = v.split(' ');
  },
});

// watch
watch(count, (newVal, oldVal) => {}); // immediate: false by default
watch(() => props.id, fetchUser, { immediate: true });
watchEffect(() => {
  console.log(count.value);
}); // auto-tracks deps
```

---

## Composables (Custom Hooks)

```ts
// useCounter.ts
export function useCounter(initial = 0) {
  const count = ref(initial);
  const increment = () => count.value++;
  const reset = () => (count.value = initial);
  return { count: readonly(count), increment, reset };
}

// useAsyncData.ts
export function useAsyncData<T>(fn: () => Promise<T>) {
  const data = ref<T | null>(null);
  const error = ref<Error | null>(null);
  const loading = ref(false);
  async function execute() {
    loading.value = true;
    try {
      data.value = await fn();
    } catch (e) {
      error.value = e as Error;
    } finally {
      loading.value = false;
    }
  }
  execute();
  return { data, error, loading, refresh: execute };
}
```

---

## Pinia

```ts
// stores/counter.ts
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0); // Setup Store (preferred)
  const doubled = computed(() => count.value * 2);
  function increment() {
    count.value++;
  }
  return { count, doubled, increment };
});

// Usage in component:
const store = useCounterStore();
// ❌ const { count } = store;        // loses reactivity!
// βœ… const count = storeToRefs(store).count;
import { storeToRefs } from 'pinia';
const { count } = storeToRefs(store);

// Persist plugin:
import { createPinia } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
const pinia = createPinia().use(piniaPluginPersistedstate);
```

---

## Vue Router 4

```ts
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: () => import('./views/Home.vue') }, // lazy-loaded
    { path: '/user/:id', component: UserView, props: true }, // props:true passes params as props
    { path: '/:pathMatch(.*)*', component: NotFound }, // 404 catch-all
  ],
});
// Route guards
router.beforeEach(async (to, from) => {
  if (to.meta.requiresAuth && !isLoggedIn()) return { name: 'Login' };
});

// In component:
import { useRouter, useRoute } from 'vue-router';
const router = useRouter();
const route = useRoute();
router.push({ name: 'User', params: { id: 42 } });
const userId = route.params.id as string;
```

---

## Templates

```vue
<template>
  <!-- v-model -->
  <input v-model="email" />
  <MyInput v-model:title="title" v-model:count="count" />
  <!-- named model -->

  <!-- v-for with key (ALWAYS set key) -->
  <li v-for="item in items" :key="item.id">{{ item.name }}</li>

  <!-- Dynamic components -->
  <component :is="currentTab" />

  <!-- Teleport β€” render in a different DOM node -->
  <Teleport to="body"><Modal v-if="showModal" /></Teleport>

  <!-- Transition -->
  <Transition name="fade" mode="out-in">
    <component :is="view" :key="view" />
  </Transition>

  <!-- Suspense (async components / composables with await) -->
  <Suspense><AsyncComponent /><template #fallback>Loading...</template></Suspense>
</template>

<style>
/* Transition CSS */
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s;
}
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>
```

---

## Nuxt 4

```
auto-imports:    ref, computed, useRoute, useFetch β€” no imports needed
composables/:    auto-imported by filename
server/api/:     server routes (GET/POST)
pages/:          file-based routing
layouts/:        layout components
middleware/:     route guards
```

```ts
// pages/users/[id].vue
const { id } = useRoute().params; // auto-imported
const { data, error, refresh } = await useFetch(`/api/users/${id}`, {
  lazy: false, // SSR: wait for data before rendering
  server: true, // fetch on server (default)
  transform: r => r.user,
});
// ❌ TRAP: useFetch in Nuxt β‰  @tanstack/react-query. It's Nuxt-specific.
// ❌ TRAP: useAsyncData key must be UNIQUE per page/component
```

---

## Performance

- βœ… Use `v-memo` for expensive list items that rarely change
- βœ… `defineAsyncComponent(() => import("./Heavy.vue"))` for code splitting
- βœ… `:key` on `<component :is>` forces re-mount on route change (prevents stale state)
- ❌ Avoid deeply nested reactive objects β€” use `shallowRef`/`shallowReactive` for large data
- ❌ Never mutate props β€” emit events instead

Attribution

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

Loading comments…