Vue.js best practices expert. PROACTIVELY use when working with Vue 3, Composition API, Pinia, Nuxt. Triggers: vue, composition API, pinia, nuxt, .vue files
/plugin marketplace add nguyenthienthanh/aura-frog/plugin install aura-frog@aurafrogThis skill is limited to using the following tools:
Expert-level Vue 3 patterns, Composition API, state management, and performance optimization.
This skill activates when:
.vue filesvue in package.json<!-- ✅ GOOD - script setup syntax -->
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import type { User } from '@/types';
// Props with defaults
interface Props {
user: User;
showAvatar?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
showAvatar: true,
});
// Emits with types
const emit = defineEmits<{
select: [user: User];
update: [id: string, data: Partial<User>];
}>();
// Reactive state
const isLoading = ref(false);
const items = ref<Item[]>([]);
// Computed
const fullName = computed(() => `${props.user.firstName} ${props.user.lastName}`);
// Methods
function handleSelect() {
emit('select', props.user);
}
// Lifecycle
onMounted(async () => {
isLoading.value = true;
items.value = await fetchItems();
isLoading.value = false;
});
</script>
// ✅ GOOD - Reusable composable
// composables/useUser.ts
import { ref, computed } from 'vue';
import type { User } from '@/types';
export function useUser(userId: Ref<string>) {
const user = ref<User | null>(null);
const isLoading = ref(false);
const error = ref<Error | null>(null);
const fullName = computed(() => {
if (user.value == null) return '';
return `${user.value.firstName} ${user.value.lastName}`;
});
async function fetchUser() {
isLoading.value = true;
error.value = null;
try {
user.value = await api.getUser(userId.value);
} catch (e) {
error.value = e instanceof Error ? e : new Error('Unknown error');
} finally {
isLoading.value = false;
}
}
watch(userId, fetchUser, { immediate: true });
return {
user: readonly(user),
fullName,
isLoading: readonly(isLoading),
error: readonly(error),
refetch: fetchUser,
};
}
// Usage
const userId = ref('123');
const { user, fullName, isLoading } = useUser(userId);
// ✅ GOOD - ref for primitives
const count = ref(0);
const name = ref('');
const isActive = ref(false);
// ✅ GOOD - ref for objects (consistent .value)
const user = ref<User | null>(null);
user.value = { id: '1', name: 'John' };
// ⚠️ CAUTION - reactive loses reactivity on reassignment
const state = reactive({ user: null });
state.user = newUser; // ✅ Works
// state = { user: newUser }; // ❌ Loses reactivity!
// ✅ GOOD - Use ref for replaceable objects
const user = ref<User | null>(null);
user.value = newUser; // ✅ Works
// ✅ GOOD - Watch single ref
watch(userId, async (newId, oldId) => {
if (newId !== oldId) {
await fetchUser(newId);
}
});
// ✅ GOOD - Watch multiple sources
watch(
[userId, companyId],
async ([newUserId, newCompanyId]) => {
await fetchData(newUserId, newCompanyId);
}
);
// ✅ GOOD - watchEffect for auto-tracking
watchEffect(async () => {
// Automatically tracks userId.value
const data = await fetchUser(userId.value);
user.value = data;
});
// ✅ GOOD - Cleanup in watch
watch(userId, async (newId, oldId, onCleanup) => {
const controller = new AbortController();
onCleanup(() => controller.abort());
const data = await fetchUser(newId, { signal: controller.signal });
user.value = data;
});
<template>
<!-- ❌ BAD - Implicit truthy check -->
<div v-if="userName">{{ userName }}</div>
<!-- ✅ GOOD - Explicit check -->
<div v-if="userName != null && userName !== ''">{{ userName }}</div>
<!-- ✅ GOOD - v-show for frequent toggles -->
<div v-show="isVisible">Frequently toggled content</div>
<!-- ✅ GOOD - v-if for conditional rendering -->
<div v-if="isLoaded">Rendered once</div>
<!-- ✅ GOOD - Template for multiple elements -->
<template v-if="items.length > 0">
<h2>Items</h2>
<ul>
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</ul>
</template>
<EmptyState v-else />
</template>
<template>
<!-- ❌ BAD - Index as key -->
<li v-for="(item, index) in items" :key="index">{{ item.name }}</li>
<!-- ✅ GOOD - Unique ID as key -->
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
<!-- ✅ GOOD - v-for with v-if (separate element) -->
<template v-for="item in items" :key="item.id">
<li v-if="item.isVisible">{{ item.name }}</li>
</template>
<!-- ✅ GOOD - Computed for filtering -->
<li v-for="item in visibleItems" :key="item.id">{{ item.name }}</li>
</template>
<script setup lang="ts">
const visibleItems = computed(() => items.value.filter(item => item.isVisible));
</script>
<template>
<!-- ✅ GOOD - Inline for simple -->
<button @click="count++">Increment</button>
<!-- ✅ GOOD - Method reference -->
<button @click="handleClick">Click</button>
<!-- ✅ GOOD - With event -->
<input @input="handleInput($event)" />
<!-- ✅ GOOD - Modifiers -->
<form @submit.prevent="handleSubmit">
<input @keyup.enter="submit" />
<div @click.stop="handleClick">
</template>
<script setup lang="ts">
// ✅ GOOD - Type-based props
interface Props {
// Required
id: string;
user: User;
// Optional with type
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
// With default (use withDefaults)
variant?: 'primary' | 'secondary';
}
const props = withDefaults(defineProps<Props>(), {
size: 'md',
disabled: false,
variant: 'primary',
});
</script>
<!-- ParentComponent.vue -->
<template>
<Card>
<template #header>
<h2>Title</h2>
</template>
<template #default>
<p>Main content</p>
</template>
<template #footer="{ canSubmit }">
<button :disabled="!canSubmit">Submit</button>
</template>
</Card>
</template>
<!-- Card.vue -->
<template>
<div class="card">
<header v-if="$slots.header">
<slot name="header" />
</header>
<main>
<slot />
</main>
<footer v-if="$slots.footer">
<slot name="footer" :canSubmit="isValid" />
</footer>
</div>
</template>
<!-- ✅ GOOD - Expose specific methods/refs -->
<script setup lang="ts">
const inputRef = ref<HTMLInputElement | null>(null);
function focus() {
inputRef.value?.focus();
}
function reset() {
// Reset logic
}
// Only expose what's needed
defineExpose({
focus,
reset,
});
</script>
// stores/user.ts
import { defineStore } from 'pinia';
// ✅ GOOD - Setup store syntax (Composition API style)
export const useUserStore = defineStore('user', () => {
// State
const user = ref<User | null>(null);
const isLoading = ref(false);
// Getters
const isAuthenticated = computed(() => user.value != null);
const fullName = computed(() => {
if (user.value == null) return '';
return `${user.value.firstName} ${user.value.lastName}`;
});
// Actions
async function login(credentials: Credentials) {
isLoading.value = true;
try {
user.value = await authApi.login(credentials);
} finally {
isLoading.value = false;
}
}
function logout() {
user.value = null;
}
return {
// State
user: readonly(user),
isLoading: readonly(isLoading),
// Getters
isAuthenticated,
fullName,
// Actions
login,
logout,
};
});
<script setup lang="ts">
import { useUserStore } from '@/stores/user';
import { storeToRefs } from 'pinia';
const userStore = useUserStore();
// ✅ GOOD - storeToRefs for reactive destructuring
const { user, isAuthenticated, fullName } = storeToRefs(userStore);
// Actions don't need storeToRefs
const { login, logout } = userStore;
</script>
// ✅ GOOD - Computed for derived state (cached)
const sortedItems = computed(() => {
return [...items.value].sort((a, b) => a.name.localeCompare(b.name));
});
// ❌ BAD - Method called in template (no caching)
function getSortedItems() {
return [...items.value].sort((a, b) => a.name.localeCompare(b.name));
}
<template>
<!-- ✅ GOOD - Static content -->
<footer v-once>
<p>Copyright 2024</p>
</footer>
<!-- ✅ GOOD - Memoize expensive renders -->
<div v-for="item in items" :key="item.id" v-memo="[item.id, item.selected]">
<ExpensiveComponent :item="item" />
</div>
</template>
// ✅ GOOD - Lazy load components
const HeavyComponent = defineAsyncComponent(() =>
import('./components/HeavyComponent.vue')
);
// ✅ GOOD - With loading/error states
const AsyncModal = defineAsyncComponent({
loader: () => import('./Modal.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorDisplay,
delay: 200,
timeout: 3000,
});
<script setup lang="ts">
import { useForm } from 'vee-validate';
import { toTypedSchema } from '@vee-validate/zod';
import { z } from 'zod';
const schema = toTypedSchema(
z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Min 8 characters'),
})
);
const { handleSubmit, errors, defineField } = useForm({
validationSchema: schema,
});
const [email, emailAttrs] = defineField('email');
const [password, passwordAttrs] = defineField('password');
const onSubmit = handleSubmit(async (values) => {
await login(values);
});
</script>
<template>
<form @submit="onSubmit">
<input v-model="email" v-bind="emailAttrs" type="email" />
<span v-if="errors.email">{{ errors.email }}</span>
<input v-model="password" v-bind="passwordAttrs" type="password" />
<span v-if="errors.password">{{ errors.password }}</span>
<button type="submit">Login</button>
</form>
</template>
// ✅ GOOD - Import component type
import type { Component } from 'vue';
import MyComponent from './MyComponent.vue';
// ✅ GOOD - Template ref typing
const modalRef = ref<InstanceType<typeof MyComponent> | null>(null);
// ✅ GOOD - Global component types (env.d.ts)
declare module 'vue' {
export interface GlobalComponents {
RouterLink: typeof import('vue-router')['RouterLink'];
RouterView: typeof import('vue-router')['RouterView'];
}
}
checklist[12]{pattern,best_practice}:
Script,Use script setup lang="ts"
State,ref for primitives and objects
Props,withDefaults + defineProps<Props>()
Emits,defineEmits with typed events
Computed,Use for derived reactive state
Watch,Use onCleanup for async
Templates,Explicit v-if checks
Keys,Unique IDs not indices
Store,Pinia setup store syntax
Store refs,storeToRefs for destructuring
Async,defineAsyncComponent for lazy load
Forms,VeeValidate + Zod
Version: 1.3.0
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.