| name | vue-builder |
| description | Expert Vue.js development covering Composition API patterns, Pinia state management, Vue Router, composables design, Nuxt.js integration, reactivity internals, and advanced component patterns.
Use when the user asks about vue builder, vue builder best practices, or needs guidance on vue builder implementation.
Do NOT use when the user needs a different specialized skill or is asking about an unrelated technology domain.
|
| license | Apache-2.0 |
| metadata | {"author":"foundry-skills","version":"1.0.0","tags":"web-development frontend javascript","category":"web-development","subcategory":"frontend-frameworks","depends":"","disclaimer":"none","difficulty":"intermediate"} |
Vue Builder
Purpose
Guide the development of robust Vue.js applications using modern patterns. This skill covers Vue 3 Composition API, state management with Pinia, routing, composable design, and Nuxt.js for full-stack Vue development.
Composition API Patterns
Script Setup (Preferred Syntax)
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue';
import { useUserStore } from '@/stores/user';
import type { User } from '@/types';
// Props with defaults
const props = withDefaults(defineProps<{
userId: string;
showAvatar?: boolean;
}>(), {
showAvatar: true,
});
// Emits with type safety
const emit = defineEmits<{
select: [user: User];
delete: [userId: string];
}>();
// Expose for parent ref access (use sparingly)
defineExpose({ refresh });
// Reactive state
const searchQuery = ref('');
const isLoading = ref(false);
// Computed
const filteredUsers = computed(() =>
users.value.filter(u => u.name.includes(searchQuery.value))
);
// Watchers
watch(() => props.userId, async (newId) => {
await fetchUser(newId);
}, { immediate: true });
// Lifecycle
onMounted(() => {
initializeComponent();
});
// Methods
async function refresh() {
isLoading.value = true;
try {
await fetchData();
} finally {
isLoading.value = false;
}
}
</script>
Options API to Composition API Migration Map
Options API -> Composition API
-------------------------------------------
data() -> ref() / reactive()
computed: {} -> computed()
methods: {} -> plain functions
watch: {} -> watch() / watchEffect()
created() -> <script setup> top level
mounted() -> onMounted()
beforeUnmount() -> onBeforeUnmount()
mixins -> composables
provide/inject -> provide() / inject()
this.$emit -> defineEmits / emit()
this.$refs -> template refs with ref()
Composables Design
Composable Rules
- Name with
use prefix: useCounter, useFetch, useAuth
- Accept refs or plain values as arguments (use
toValue() internally)
- Return refs (not raw values) so consumers maintain reactivity
- Handle cleanup in
onBeforeUnmount or return a cleanup function
- Keep composables focused on a single concern
Standard Composable Template
import { ref, watchEffect, toValue, type Ref, type MaybeRefOrGetter } from 'vue';
interface UseFetchOptions {
immediate?: boolean;
refetch?: boolean;
}
interface UseFetchReturn<T> {
data: Ref<T | null>;
error: Ref<Error | null>;
isLoading: Ref<boolean>;
execute: () => Promise<void>;
}
export function useFetch<T>(
url: MaybeRefOrGetter<string>,
options: UseFetchOptions = {}
): UseFetchReturn<T> {
const { immediate = true, refetch = true } = options;
const data = ref<T | null>(null) as Ref<T | null>;
const error = ref<Error | null>(null);
const isLoading = ();
() {
isLoading. = ;
error. = ;
{
response = ((url));
(!response.) ();
data. = response.();
} (e) {
error. = e ;
} {
isLoading. = ;
}
}
(refetch) {
( {
(url);
();
});
} (immediate) {
();
}
{ data, error, isLoading, execute };
}
Composable Composition (Layered Architecture)
export function useApi<T>(endpoint: MaybeRefOrGetter<string>) { ... }
export function useUsers(filters: MaybeRefOrGetter<UserFilters>) {
const url = computed(() => `/api/users?${buildQuery(toValue(filters))}`);
const { data, error, isLoading, execute } = useApi<User[]>(url);
const activeUsers = computed(() =>
(data.value ?? []).filter(u => u.status === 'active')
);
return { users: data, activeUsers, error, isLoading, refetch: execute };
}
export function useUserSearch() {
const query = ref('');
const debouncedQuery = refDebounced(query, 300);
const filters = computed<UserFilters>(() => ({ search: debouncedQuery.value }));
const { users, activeUsers, isLoading } = (filters);
{ query, users, activeUsers, isLoading };
}
Pinia State Management
Store Design
import { defineStore } from 'pinia';
export const useAuthStore = defineStore('auth', () => {
const user = ref<User | null>(null);
const token = ref<string | null>(localStorage.getItem('token'));
const isAuthenticated = computed(() => !!token.value && !!user.value);
const userRole = computed(() => user.value?.role ?? 'guest');
async function login(credentials: LoginCredentials) {
const response = await authApi.login(credentials);
token.value = response.token;
user.value = response.user;
localStorage.setItem('token', response.token);
}
function () {
token. = ;
user. = ;
.();
}
() {
(!token.) ;
{
user. = authApi.();
} {
();
}
}
{ user, token, isAuthenticated, userRole, login, logout, refreshUser };
});
Pinia Best Practices
DO:
- Use Setup Store syntax (function style) for TypeScript projects
- Keep stores focused on a single domain
- Use composables for reusable logic, stores for shared state
- Subscribe to store changes for side effects: store.$subscribe()
- Use storeToRefs() to destructure state while keeping reactivity
DON'T:
- Put UI state (modals, tooltips) in Pinia -- use local component state
- Create deeply nested store structures
- Mutate state outside of actions in production
- Use Pinia for server state -- use TanStack Query/VueQuery instead
storeToRefs Usage
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { useAuthStore } from '@/stores/auth';
const authStore = useAuthStore();
// Destructure reactive state with storeToRefs
const { user, isAuthenticated } = storeToRefs(authStore);
// Actions can be destructured directly (no reactivity needed)
const { login, logout } = authStore;
</script>
Vue Router Patterns
Route Organization
const routes: RouteRecordRaw[] = [
{
path: '/',
component: () => import('@/layouts/DefaultLayout.vue'),
children: [
{ path: '', name: 'home', component: () => import('@/pages/Home.vue') },
{ path: 'about', name: 'about', component: () => import('@/pages/About.vue') },
],
},
{
path: '/dashboard',
component: () => import('@/layouts/DashboardLayout.vue'),
meta: { requiresAuth: true },
children: [
{ path: '', name: 'dashboard', component: () => import('@/pages/Dashboard.vue') },
{ path: 'settings', name: 'settings', component: () => import('@/pages/Settings.vue') },
],
},
{ path: , : , : () },
];
Navigation Guards
router.beforeEach(async (to, from) => {
const authStore = useAuthStore();
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
return { name: 'login', query: { redirect: to.fullPath } };
}
if (to.name === 'login' && authStore.isAuthenticated) {
return { name: 'dashboard' };
}
if (to.meta.requiredRole && authStore.userRole !== to.meta.requiredRole) {
return { name: 'forbidden' };
}
});
Reactivity Deep Dive
ref vs reactive Decision
Use ref() when:
- Storing primitives (string, number, boolean)
- You need to reassign the entire value (ref.value = newArray)
- Consistency (ref works for everything)
Use reactive() when:
- Storing objects where you never reassign the root
- You want to avoid .value syntax in script
- Grouping related state (like a form object)
PREFER ref() as the default -- it is more predictable and consistent.
Reactivity Gotchas
const store = useCounterStore();
const { count } = store;
const { count } = storeToRefs(store);
const state = reactive({ count: 0 });
state = reactive({ count: 1 });
const list = ref<string[]>([]);
list.value.push('item');
list.value[0] = 'changed';
import { toRaw } from 'vue';
const rawData = toRaw(reactiveData);
Provide / Inject Patterns
Typed Provide/Inject
import type { InjectionKey, Ref } from 'vue';
export const ThemeKey: InjectionKey<Ref<'light' | 'dark'>> = Symbol('theme');
export const NotificationKey: InjectionKey<{
notify: (msg: string, type: 'success' | 'error') => void;
}> = Symbol('notification');
import { provide, ref } from 'vue';
import { ThemeKey } from '@/injection-keys';
const theme = ref<'light' | 'dark'>('light');
provide(ThemeKey, theme);
import { inject } from 'vue';
import { ThemeKey } from '@/injection-keys';
const theme = ();
(!theme) ();
Teleport Usage
<!-- Modal teleported to body -->
<template>
<button @click="showModal = true">Open</button>
<Teleport to="body">
<Transition name="fade">
<div v-if="showModal" class="modal-overlay" @click.self="showModal = false">
<div class="modal-content" role="dialog" aria-modal="true">
<slot />
<button @click="showModal = false">Close</button>
</div>
</div>
</Transition>
</Teleport>
</template>
<!-- Conditional teleport target -->
<Teleport :to="isMobile ? '#mobile-nav' : '#desktop-nav'" :disabled="!shouldTeleport">
<NavMenu />
</Teleport>
Nuxt.js Integration
Nuxt 3 Directory Conventions
app/
components/ # Auto-imported components
ui/ # <UiButton /> (prefix-based)
composables/ # Auto-imported composables
layouts/ # <NuxtLayout name="admin">
middleware/ # Route middleware
pages/ # File-based routing
plugins/ # App plugins
server/ # Server routes, middleware, API
api/ # /api/* endpoints
middleware/ # Server middleware
utils/ # Auto-imported utility functions
app.vue # Root component
nuxt.config.ts # Configuration
Server API Routes (Nuxt)
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id');
const user = await db.user.findUnique({ where: { id } });
if (!user) throw createError({ statusCode: 404, message: 'User not found' });
return user;
});
const { data: user, pending, error } = await useFetch(`/api/users/${userId}`);
Nuxt Middleware
export default defineNuxtRouteMiddleware((to, from) => {
const { isAuthenticated } = useAuthStore();
if (!isAuthenticated) {
return navigateTo('/login', { redirectCode: 302 });
}
});
Project Architecture Checklist
When to Use
Use this skill when:
- Designing or implementing vue builder solutions
- Reviewing or improving existing vue builder approaches
- Making architectural or implementation decisions about vue builder
- Learning vue builder patterns and best practices
- Troubleshooting vue builder-related issues
Do NOT use this skill when:
- The question is about a fundamentally different technology domain
- A more specific sibling skill covers the exact topic needed
- The user needs a complete hands-on tutorial rather than expert guidance
Output Format
# Vue Builder Analysis
## Context Assessment
[Situation summary and constraints]
## Recommended Approach
[Primary recommendation with rationale]
## Implementation Steps
1. [Step with specific details]
2. [Step with specific details]
3. [Step with specific details]
## Trade-offs and Considerations
- [Key trade-off 1]
- [Key trade-off 2]
## Next Steps
- [Immediate action item]
- [Follow-up action item]
Example
Input: "Help me implement vue builder for a medium-scale production application"
Output: A structured analysis covering current state assessment, recommended vue builder approach with specific patterns, implementation roadmap with milestones, and risk mitigation strategies tailored to the application scale and constraints.
Edge Cases
- Legacy system integration: When vue builder must coexist with legacy approaches, provide a gradual migration path rather than a complete rewrite
- Scale mismatch: When the solution complexity exceeds the project scale, recommend a simpler approach and note when to revisit
- Team skill gaps: When the team lacks experience with the recommended approach, include learning resources and simpler alternatives
- Conflicting requirements: When constraints conflict (e.g., performance vs. maintainability), explicitly state the trade-off and recommend based on stated priorities