Back to skills
SKILL.md
Vue Expert
ASecurityUse 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
Works with
Security analysis
100/100npx -y skills add Harmitx7/tribunal-kit --skill vue-expert --agent claude-codeAre you the author of Vue Expert?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/harmitx7-vue-expert-tribunal-kit)---
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
Comments
Loading commentsβ¦