Содержание

Во 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 для каждого теста, вызываются действия и проверяется состояние. Для компонентов есть утилита, подменяющая стор тестовым.