10. Управление состоянием (useState, Pinia)
🗃️ Управление состоянием в Nuxt 3
Заголовок раздела «🗃️ Управление состоянием в Nuxt 3»Nuxt 3 предоставляет несколько инструментов для управления состоянием. Ключевое отличие от обычного Vue — состояние должно корректно работать при SSR: сериализоваться на сервере и гидрироваться на клиенте без несоответствий.
Проблема состояния в SSR 🌐
Заголовок раздела «Проблема состояния в SSR 🌐»// ❌ Обычный ref — НЕ работает для глобального состояния в SSRconst count = ref(0)// Каждый запрос создаёт новый ref// Состояние не передаётся с сервера на клиент
// ✅ useState — работает правильноconst count = useState('counter', () => 0)// Уникальный ключ 'counter' — один экземпляр// Данные сериализуются в HTML и гидрируются на клиентеuseState — встроенное решение 🔑
Заголовок раздела «useState — встроенное решение 🔑»export const useAppState = () => { // Состояние с ключом — одинаково на сервере и клиенте const user = useState<User | null>('app-user', () => null) const theme = useState<'light' | 'dark'>('app-theme', () => 'dark') const lang = useState<string>('app-lang', () => 'ru')
return { user, theme, lang }}<!-- Любой компонент — одно и то же состояние --><script setup>const { user, theme } = useAppState()</script>Pinia в Nuxt 3 🍍
Заголовок раздела «Pinia в Nuxt 3 🍍»Pinia — официальный стейт-менеджер Vue 3. С @pinia/nuxt она работает из коробки и корректно с SSR.
Установка
Заголовок раздела «Установка»npm install pinia @pinia/nuxtexport default defineNuxtConfig({ modules: ['@pinia/nuxt'],
// Автоимпорт stores pinia: { storesDirs: ['./stores/**'], }})Создание Store 🏪
Заголовок раздела «Создание Store 🏪»export const useCounterStore = defineStore('counter', () => { // State const count = ref(0) const history = ref<number[]>([])
// Getters (computed) const doubled = computed(() => count.value * 2) const isPositive = computed(() => count.value > 0)
// Actions const increment = () => { history.value.push(count.value) count.value++ }
const decrement = () => { history.value.push(count.value) count.value-- }
const reset = () => { history.value.push(count.value) count.value = 0 }
const undo = () => { const prev = history.value.pop() if (prev !== undefined) count.value = prev }
return { count, doubled, isPositive, history, increment, decrement, reset, undo }})Options Store (альтернативный синтаксис) 📋
Заголовок раздела «Options Store (альтернативный синтаксис) 📋»export const useAuthStore = defineStore('auth', { // State state: () => ({ user: null as User | null, token: null as string | null, loading: false, }),
// Getters getters: { isLoggedIn: (state) => !!state.user, isAdmin: (state) => state.user?.role === 'admin', fullName: (state) => state.user ? \`\${state.user.firstName} \${state.user.lastName}\` : '', },
// Actions actions: { async login(credentials: { email: string; password: string }) { this.loading = true try { const response = await $fetch('/api/auth/login', { method: 'POST', body: credentials, }) this.user = response.user this.token = response.token await navigateTo('/dashboard') } finally { this.loading = false } },
async logout() { await $fetch('/api/auth/logout', { method: 'POST' }) this.user = null this.token = null await navigateTo('/login') },
async fetchUser() { try { this.user = await $fetch('/api/auth/me') } catch { this.user = null } } }})Использование Store в Компонентах 🧩
Заголовок раздела «Использование Store в Компонентах 🧩»<script setup lang="ts">const authStore = useAuthStore()const counterStore = useCounterStore()
// Деструктуризация с storeToRefs (сохраняет реактивность!)const { user, isLoggedIn, isAdmin } = storeToRefs(authStore)const { count, doubled } = storeToRefs(counterStore)
// Actions можно деструктурировать напрямуюconst { login, logout } = authStoreconst { increment, decrement } = counterStore</script>
<template> <div v-if="isLoggedIn"> <h1>Привет, {{ user?.name }}!</h1> <p v-if="isAdmin">Ты администратор 👑</p>
<div> <p>Счётчик: {{ count }} (x2: {{ doubled }})</p> <button @click="increment">+</button> <button @click="decrement">-</button> </div>
<button @click="logout">Выйти</button> </div></template>Persisting State — сохранение между сессиями 💾
Заголовок раздела «Persisting State — сохранение между сессиями 💾»npm install pinia-plugin-persistedstatenpm install @pinia-plugin-persistedstate/nuxtexport default defineNuxtConfig({ modules: ['@pinia/nuxt', '@pinia-plugin-persistedstate/nuxt']})export const useSettingsStore = defineStore('settings', () => { const theme = ref<'light' | 'dark'>('dark') const language = ref('ru') const sidebarOpen = ref(true)
return { theme, language, sidebarOpen }}, { // Сохраняем в localStorage persist: true,
// Или с детальными настройками: persist: { storage: persistedState.localStorage, pick: ['theme', 'language'], // Только эти поля }})SSR с Pinia — гидрация данных 🖥️
Заголовок раздела «SSR с Pinia — гидрация данных 🖥️»export const useProductsStore = defineStore('products', () => { const items = ref<Product[]>([]) const loading = ref(false)
const fetchProducts = async () => { loading.value = true items.value = await $fetch('/api/products') loading.value = false }
return { items, loading, fetchProducts }})<!-- pages/products.vue — серверная загрузка данных в store --><script setup>const store = useProductsStore()
// callOnce — выполняется один раз (и на сервере, и на клиенте)// Данные гидрируются автоматическиawait callOnce(store.fetchProducts)</script>Store Composition — вложение Store 🧱
Заголовок раздела «Store Composition — вложение Store 🧱»export const useCartStore = defineStore('cart', () => { // Используем другой store внутри! const authStore = useAuthStore()
const items = ref<CartItem[]>([])
const total = computed(() => items.value.reduce((sum, item) => sum + item.price * item.qty, 0) )
const checkout = async () => { if (!authStore.isLoggedIn) { await navigateTo('/login') return } await $fetch('/api/orders', { method: 'POST', body: { items: items.value } }) items.value = [] }
return { items, total, checkout }})useNuxtApp.$pinia — прямой доступ к Pinia 🎛️
Заголовок раздела «useNuxtApp.$pinia — прямой доступ к Pinia 🎛️»export default defineNuxtPlugin(() => { const { $pinia } = useNuxtApp()
// Подписываемся на все изменения всех stores $pinia.use(({ store }) => { store.$subscribe((mutation) => { // Отправляем события в аналитику if (mutation.type === 'direct') { analytics.track('store-mutation', { store: mutation.storeId, key: mutation.events.key, newValue: mutation.events.newValue, }) } }) })})Hydration с useState 💧
Заголовок раздела «Hydration с useState 💧»// Как данные путешествуют с сервера на клиент:
// 1. На сервере:const counter = useState('counter', () => 42)// → сериализуется в HTML: window.__NUXT__ = { state: { counter: 42 } }
// 2. В HTML документе:// <script>window.__NUXT__ = { state: { counter: 42 } }</script>
// 3. На клиенте при гидрации:const counter = useState('counter', () => 42)// → берёт 42 из window.__NUXT__.state.counter// → initializer () => 42 НЕ вызывается!