Содержание
Во Vue 3 большую часть состояния стоит держать как можно ближе к месту использования: внутри компонента или в composable-функции. Pinia нужна для действительно общего клиентского состояния, которое живёт дольше одного экрана: пользователь, корзина, настройки интерфейса. Данные с сервера лучше не складывать в стор вручную, а загружать инструментами с кешированием.
Разберём, какое состояние бывает, когда хватает composables, когда нужна Pinia и как не столкнуться с проблемами при серверном рендеринге.
Какое состояние бывает во фронтенд-приложении?
Почти все проблемы с состоянием начинаются с того, что разные виды данных хранят одинаково. Полезно сразу разделять их по происхождению и сроку жизни.
| Вид состояния | Пример | Где хранить |
|---|---|---|
| Локальное | Открыт ли выпадающий список, значение поля | Внутри компонента |
| Состояние формы | Черновик заказа на несколько шагов | Composable или компонент-обёртка |
| Данные с сервера | Каталог, список заказов | Загрузка с кешем: useFetch, TanStack Query |
| Глобальное клиентское | Текущий пользователь, корзина, тема | Pinia |
| Состояние в адресе | Фильтры, страница, сортировка | URL — чтобы ссылкой можно было поделиться |
Когда хватает composables?
Composable — это функция, которая использует реактивность Vue и возвращает состояние и методы. Её хватает, когда логику нужно переиспользовать в нескольких компонентах, но каждому компоненту нужна своя копия состояния: пагинация, работа с формой, таймер, загрузка файла.
export function usePagination(total: Ref<number>, perPage = 20) {
const page = ref(1)
const pages = computed(() => Math.ceil(total.value / perPage))
const next = () => { if (page.value < pages.value) page.value++ }
const prev = () => { if (page.value > 1) page.value-- }
return { page, pages, next, prev }
}
Composable можно сделать и общим, если вынести ref за пределы функции, но так легко получить утечку состояния между пользователями при серверном рендеринге. Для общего состояния надёжнее Pinia.
Фото: Unsplash
Когда нужна Pinia?
Pinia нужна, когда одно и то же клиентское состояние используют разные части приложения, оно живёт дольше одного экрана и его изменения важно отслеживать. Pinia даёт понятную структуру, инструменты разработчика с историей изменений, типизацию и корректную работу с SSR.
export const useCartStore = defineStore('cart', () => {
const items = ref<CartItem[]>([])
const total = computed(() => items.value.reduce((sum, i) => sum + i.price * i.qty, 0))
function add(product: Product) {
const item = items.value.find(i => i.id === product.id)
item ? item.qty++ : items.value.push({ ...product, qty: 1 })
}
return { items, total, add }
})
Типичные кандидаты для стора:
- авторизованный пользователь и его права;
- корзина и избранное;
- настройки интерфейса: тема, язык, свёрнутое меню;
- состояние, которое синхронизируется между вкладками или с локальным хранилищем.
Почему не стоит хранить данные сервера в сторе?
Если вручную загружать данные в стор, приходится самому решать, когда их обновлять, как показывать загрузку и ошибку, как не загрузить одно и то же дважды и как сбросить устаревшее. Инструменты загрузки данных с кешированием делают это из коробки.
- в Nuxt —
useFetchиuseAsyncData: данные загружаются на сервере и передаются в браузер без повторного запроса; - в обычном Vue — TanStack Query: кеш, повторы, фоновое обновление, инвалидация;
- в стор попадает только то, что пользователь изменил на клиенте и ещё не сохранил.
Про API, к которому обращается фронтенд, — статья как спроектировать REST API.
Как избежать проблем с SSR?
При серверном рендеринге одно серверное приложение обслуживает много пользователей. Если состояние объявлено на уровне модуля, данные одного пользователя могут попасть в страницу другого. Pinia и встроенные инструменты Nuxt создают отдельное состояние на каждый запрос.
- не объявляйте реактивное состояние на уровне модуля вне сторов и
useState; - не обращайтесь к
windowиlocalStorageво время серверного рендеринга; - не кладите в стор то, что не должно попасть в HTML страницы, — секреты и чужие данные;
- следите, чтобы серверное и клиентское состояние совпадали, иначе будут ошибки гидратации.
Подробнее о Nuxt — статьи SSR в Nuxt и SEO и Vue или React, а о типизации — TypeScript на фронтенде.
Коротко
- Держите состояние как можно ближе к месту использования.
- Composables — для переиспользуемой логики со своим состоянием в каждом компоненте.
- Pinia — для общего клиентского состояния: пользователь, корзина, настройки.
- Данные с сервера загружайте инструментами с кешем, а не складывайте в стор вручную.
- При SSR не держите состояние на уровне модуля и не обращайтесь к браузерным API на сервере.
Частые вопросы
Pinia или Vuex?
Для новых проектов — Pinia: она официально рекомендована командой Vue, проще, лучше типизируется и поддерживает Composition API. Vuex поддерживается для старых проектов, но новых возможностей в нём не развивают.
Нужно ли хранить данные с сервера в Pinia?
Лучше использовать инструменты загрузки данных с кешированием — useFetch и useAsyncData в Nuxt или библиотеки вроде TanStack Query. Они сами решают вопросы обновления, повторов и устаревания данных.
Как тестировать сторы Pinia?
Сторы тестируются как обычные модули: создаётся новый экземпляр Pinia для каждого теста, вызываются действия и проверяется состояние. Для компонентов есть утилита, подменяющая стор тестовым.



