---
paths:
- "**/*.vue"
- "**/components/**/*.ts"
- "**/composables/**/*.ts"
- "**/stores/**/*.ts"
- "**/pages/**/*.vue"
---
# Vue Patterns
> This file extends [typescript/patterns.md](../typescript/patterns.md) and [common/patterns.md](../common/patterns.md) with Vue-specific architecture patterns. For composable rules see [hooks.md](./hooks.md).
## Component Design Principles
### Presentational vs Container
Split large views into container (data-fetching, state, orchestration) and presentational (props-in, events-out) components.
```vue
{{ user.name }}
```
### Provide / Inject
Use for dependency injection (not state management). Ideal for: theme, locale, configuration, plugin API surfaces.
```ts
// Provider — in a parent or plugin
const theme = ref("light");
provide("theme", readonly(theme));
// Consumer — in any descendant
const theme = inject>("theme");
```
- Always use `readonly()` when providing to prevent child mutations.
- Use `Symbol` keys for injection to avoid name collisions.
- Document the injection key type with a shared constant.
### Scoped Slots
Use scoped slots when a child component owns data but the parent controls rendering.
```vue
```
## State Management
### Decision Tree
1. **Component-local**: `ref()` / `reactive()` inside the component
2. **Shared between parent + few children**: Lift to parent, pass via props + emits
3. **Shared across distant branches, infrequent updates**: `provide` / `inject`
4. **Global, shared, complex**: Pinia store
5. **Server-derived data**: Composables wrapping `fetch` / `useFetch` (Nuxt) / TanStack Query (Vue Query)
### Pinia Patterns
```ts
// stores/useUserStore.ts
import { defineStore } from "pinia";
import { ref, computed } from "vue";
import { getUser, updateUser } from "@/api/user";
export const useUserStore = defineStore("user", () => {
// State
const currentUser = ref(null);
const isLoading = ref(false);
const error = ref(null);
// Getters (computed)
const isLoggedIn = computed(() => currentUser.value !== null);
const displayName = computed(() =>
currentUser.value ? currentUser.value.name : "Guest"
);
// Actions
async function fetchUser(id: string) {
isLoading.value = true;
error.value = null;
try {
currentUser.value = await getUser(id);
} catch (e) {
error.value = e as Error;
} finally {
isLoading.value = false;
}
}
return { currentUser, isLoading, error, isLoggedIn, displayName, fetchUser };
});
```
- Prefer **Setup Store** syntax (Composition API) over Options Store.
- Store actions are the ONLY place to mutate state — no direct `store.$patch` in components for complex logic.
- Every async action must handle loading, success, and error states.
- Keep stores focused on one domain — split auth, user, cart, etc. into separate stores.
## Vue Router Patterns
### Navigation Guards
```ts
// Global guard
router.beforeEach((to, from) => {
const store = useUserStore();
if (to.meta.requiresAuth && !store.isLoggedIn) {
return { name: "login", query: { redirect: to.fullPath } };
}
});
```
- Always provide a redirect path so the user returns to their intended destination after login.
- Route guards should not have side effects beyond navigation decisions.
- Use `beforeEnter` on routes for route-specific checks; `beforeEach` for global ones.
### Lazy Loading
```ts
const routes = [
{
path: "/dashboard",
component: () => import("@/pages/Dashboard.vue"), // lazy
},
{
path: "/settings",
component: () => import("@/pages/Settings.vue"),
// Provide loading/error components
meta: {
__loadingComponent: LoadingSpinner,
__errorComponent: ErrorView,
},
},
];
```
### Route Params inside Same Component
```vue
```
## List Rendering
### `v-for` with Stable Keys
```vue
{{ item.name }}
{{ item.name }}
{{ item.name }}
```
## Forms
### v-model Patterns
```vue
```
### Form Validation
For non-trivial forms, use a vetted library:
- **VeeValidate** — declarative validation rules, form-level context.
- **FormKit** — schema-based forms with built-in validation.
- **Custom with composable** — for simple cases only.
```ts
// Anti-pattern: manual validation in component
const errors = ref([]);
function submit() {
errors.value = [];
if (!email.value.includes("@")) errors.value.push("Invalid email");
// ... fragile, not reusable, no i18n
}
```
### Event Handling
```vue
Link
```
## Scoped CSS
```vue
```
- Always use `
```
## Teleport
Use `` for modals, tooltips, notifications — content that must escape parent overflow/z-index constraints.
```vue
```
**Vue 3.5+**: `` supports `defer` prop for deferred mounting. This allows teleporting to a target element that is rendered later in the same render cycle:
```vue
Teleported content
```
## KeepAlive
Cache component state when toggling between views. Always set `:max` to control memory.
```vue
```
## `useId()` (Vue 3.5+)
Generate unique, SSR-stable IDs for form elements and accessibility attributes:
```vue
```
- IDs are unique per application instance and stable across server/client rendering.
- Prefer `useId()` over manual ID generation to avoid SSR hydration mismatches.
## `data-allow-mismatch` (Vue 3.5+)
Suppress unavoidable server/client value mismatch warnings:
```vue
{{ date.toLocaleString() }}{{ clientOnlyValue }}
```
Allowed types: `text`, `children`, `class`, `style`, `attribute`.
## Suspense (Experimental / Vue 3.3+)
```vue
```
## Dynamic Components
```vue
```
## Out of Scope (Pointer Sections)
### Nuxt-specific Patterns
Nuxt auto-imports, server routes, Nitro, modules, and build configuration are treated as a separate framework concern. When adding deep Nuxt-specific patterns, see `skills/nuxt4-patterns/` if present, or propose a dedicated `rules/nuxt/` track.
### Vue 2 / Migration
Options API, `Vue.extend`, `Vue.directive`, filters, and event bus patterns belong to migration documentation. New code should target Vue 3 Composition API.
## Skill Reference
For Vue deep dives see `skills/vue-patterns/SKILL.md`. For cross-framework frontend concerns see `skills/frontend-patterns/SKILL.md`. For accessibility see `skills/accessibility/SKILL.md`.