Стандарты
Pinia Stores
Структура stores/
app/stores/
├── user.ts # Авторизация, текущий пользователь
├── sidebar.ts # Состояние сайдбара
├── notification.ts # Уведомления
└── collection.ts # Справочники
Паттерн store (Setup Store)
// app/stores/user.ts
export const useUserStore = defineStore('user', () => {
// === State ===
const user = ref<User | null>(null)
const token = ref<string | null>(null)
// === Getters ===
const isAuthenticated = computed(() => !!token.value)
const fullName = computed(() =>
user.value
? `${user.value.firstName} ${user.value.lastName}`
: ''
)
// === Actions ===
async function login(credentials: LoginCredentials) {
const config = useRuntimeConfig()
// Login — публичный endpoint, используем $fetch напрямую
const response = await $fetch<LoginResponse>('/auth/login', {
baseURL: config.public.apiBase,
method: 'POST',
body: credentials,
})
token.value = response.token
user.value = response.user
localStorage.setItem('token', response.token)
}
function logout() {
token.value = null
user.value = null
localStorage.removeItem('token')
navigateTo('/login')
}
// === Init (SPA) ===
function init() {
const savedToken = localStorage.getItem('token')
if (savedToken) {
token.value = savedToken
fetchCurrentUser()
}
}
async function fetchCurrentUser() {
if (!token.value) return
// Авторизованный запрос — используем useAuthFetch
const authFetch = useAuthFetch()
try {
user.value = await authFetch<User>('/auth/me')
} catch {
logout()
}
}
// === Return ===
return {
user,
token,
isAuthenticated,
fullName,
login,
logout,
init,
}
})
Инициализация (SPA)
В SPA режиме используем client plugin для инициализации:
// app/plugins/init.client.ts
export default defineNuxtPlugin(() => {
const userStore = useUserStore()
userStore.init()
})
Использование в компонентах
<script setup lang="ts">
import { storeToRefs } from 'pinia'
// Store автоимпортируется
const userStore = useUserStore()
// storeToRefs для реактивности state/getters
const { user, isAuthenticated } = storeToRefs(userStore)
// Actions вызываются напрямую
function handleLogout() {
userStore.logout()
}
</script>
<template>
<div v-if="isAuthenticated">
{{ user?.firstName }}
<UButton @click="handleLogout">Выйти</UButton>
</div>
</template>
Правила
1. Используй storeToRefs
// ❌ Потеря реактивности
const { user } = userStore
// ✅ Сохранение реактивности
const { user } = storeToRefs(userStore)
2. Не мутируй state напрямую
// ❌ Плохо
userStore.user.name = 'New Name'
// ✅ Хорошо — через action
userStore.updateProfile({ name: 'New Name' })
3. Один store — одна ответственность
✅ useUserStore — пользователь и авторизация
✅ useSidebarStore — состояние UI сайдбара
✅ useOrdersStore — список заказов (если нужен глобально)
❌ useAppStore — всё в одном (слишком большой)
Примеры stores
Sidebar
// app/stores/sidebar.ts
export const useSidebarStore = defineStore('sidebar', () => {
const collapsed = ref(false)
function toggle() {
collapsed.value = !collapsed.value
}
return { collapsed, toggle }
})
Notifications
// app/stores/notification.ts
interface Notification {
id: string
type: 'success' | 'error' | 'info'
message: string
}
export const useNotificationStore = defineStore('notification', () => {
const notifications = ref<Notification[]>([])
function add(notification: Omit<Notification, 'id'>) {
const id = Date.now().toString()
notifications.value.push({ ...notification, id })
setTimeout(() => remove(id), 5000)
}
function remove(id: string) {
const index = notifications.value.findIndex(n => n.id === id)
if (index > -1) {
notifications.value.splice(index, 1)
}
}
return { notifications, add, remove }
})
Чек-лист
- Setup Store синтаксис
- storeToRefs для state/getters
- Init через client plugin (SPA)
- Один store — одна ответственность
- Типизация state и actions