Skip to content

State management во Vue 3

Зачем нужен State management?

Иногда в приложении нужно передать реактивные данные или функцию из одного компонента в другой, и эти компоненты не принадлежат одной иерархии. Механизмы пропсов/ивентов или provide/inject не подходят для этого. Поэтому во Vue 2 появился Vuex - state management библиотека, которая позволяет хранить реактивный стейт и предоставлять доступ к нему отовсюду.

Библиотека управления стейтом на фронтенде обычно называется "стором".

Как можно передать реактивные данные из одного компонента в другой во Vue 3?
  • Если один компонент является прямым потомком другого - пропсы и ивенты
  • Если один компонент непрямой потомок другого - provide/inject или проп/ивент дриллинг (плохая практика)
  • Если они в разных ветках иерархии - стор или Vue 3 ref/reactive

Vue 3 ref/reactive - это когда в отдельном js модуле вы определяете и экспортируете реактивную переменную:

export const userLoggedIn = ref(false);

После чего она доступна во всём приложении через импорт данного модуля.

Однако, расшаривать просто переменную - архитектурно плохая идея. Обычно вокруг этой переменной есть бизнес логика, которую и надо сделать доступной.

Например, для аутентификации нужен не только сам факт, что пользователь залогинился, но и сопутствующие методы - login(), logout(), register(), isAuthenticated(). Синтез некоего стейта и сопутствующей бизнес логики дал так называемые composable функции во Vue 3.

В некотором смысле - аналог объекта в ООП.

Что такое стор (store) на фронтенде?

A Store (like Pinia) is an entity holding state and business logic that isn't bound to your Component tree. In other words, it hosts global state. It's a bit like a component that is always there and that everybody can read off and write to.

Стор (например, Pinia) — это сущность, содержащая состояние и бизнес-логику, которая не привязана к вашему дереву компонентов. Другими словами, здесь находится глобальное состояние. Это что-то вроде компонента, который всегда присутствует, и который каждый может считывать и писать в него.

Официальная документация Pinia

Выглядит как очень неудачное, двусмысленное и запутывающее определение.

С точки зрения разработчика не фронтенда (по сути любой другой язык, кроме JavaScript, плюс частично JavaScript), аналогом конструкции с названиемстор будет база данных - SQL, NoSQL или кэширующий Redis. Там есть аналоги стейта и геттеров (View в SQL БД). Но в 99.99% случаях в базе данных не будет никакой бизнес логики, за исключением ограничений для консистентности данных (например, unique или foreign keys).

С точки зрения разработчика не фронтенда, сторы фронтенда - это просто объекты / stateful сервисы, построенные по шаблону синглтон. Называть их "глобальными сторами" как минимум нелогично.

Скорей всего, это произошло исторически. Сперва был один Vuex, он был один стор и глобален. Потом у него появились модули. Потом у Pinia эти модули стали независимы и расползлись по всему приложению. В итоге, в приложении куча мелких локальных (по области использования) сторов, каждый из которых считает себя глобальным, даже если его использует 2-3 компонента из 1000 на проекте, и даже если в этом сторе 1% - стейта, и 99% - бизнес и сопутствующей логики.

В этом плане использование композабл функций для той же цели (an entity holding state and business logic that isn't bound to your Component tree) - намного более логично. И называть их следует не useAuthStore, а useAuth или useAuthService.

Vuex или Pinia?

Во Vue 3 основной внешней библиотекой управления стейтом стала Pinia. В отличие от Vuex у нее есть поддержка TypeScript, она удобней и, естественно, пользуется преимуществами Vue 3.

Vuex официально устарел (deprecated)

Pinia или Composable functions?

Во Vue 3 появилось нечто, делающее отдельную библиотеку для управления стейтом ненужной. А именно, реактивные типыRef и Reactive, которыми можно пользоваться за пределами компонент. Стало возможным делать свои сторы на основе composable функций и подключать их в любом компоненте.

Основное декларируемое отличие Pinia - интеграция с Vue DevTools, плагины и SSR поддержка. Однако, нужно ли ему то или другое, каждый разработчик решает сам. Работать с composables в DevTools вполне комфортно.

Composable функция, в свою очередь, может иметь как глобальный, так и локальный (переменные объявлены внутри функции) стейты. Это бывает удобно в определенных случаях - можно создать несколько экземпляров composable функций, каждая со своим стейтом. Например, когда у вас на сайте несколько новостных виджетов, различающихся только категорией новостей.

Кроме того, с функциональной точки зрения composable функции имеют полный доступ ко всему Vue Reactivity API, что делает их гибче, чем Pinia.

Composable функции с глобальным стейтом не работают в SSR режиме.

Что касается производительности, то по тестам на изменениях Reactive Pinia примерно в полтора раза медленней Vue 3 Reactive, а на Ref - в 20 раз. Это потому, что Ref (примитивные типы) в Pinia становится частью Reactive. Setup store не решают эту проблему.

Также надо помнить, что любая зависимость (в данном случае библиотека Pinia) может принести проблемы, аналогичные с ситуацией "RIP Vuex", когда библиотека умирает, устаревает, перестает поддерживаться или в ней находят уязвимости. Composable функции, в свою очередь, выглядят основательным нововведением во Vue фреймворк.

Вот интересное обсуждение по теме на /r/vuejs. Пример кода от Reddit пользователя @ferferga показывает, как использовать классы TypeScript с private методами, геттерами, сеттерами (без .value) и поддержкой типов первого класса в качестве store на Composition API (что было бы невозможно с Pinia). Использование классов TS здесь, возможно, не является хорошей практикой, но демонстрирует гибкость и мощь Composition API.

Также вот лайфхак для Devtools от пользователя @coolcosmos: Я просто использую рефы. Минус в том, что вы теряете Devtools, но в режиме разработки я временно импортирую все свои рефы и передаю их в Pinia, так что у меня есть все плюсы и никаких минусов.

Как разделять логику между компонентом и composable функциями?

Удобно представлять это как MVC шаблон, где роль View (и частично Controller) выполняют компоненты, отвечающие преимущественно за визуализацию, а логика и модель (Model и частично Controller) приходятся на композабл функции и их реактивный стейт.

Чтобы понять, что класть в композабл, а что в компонент (который может состоять из нескольких компонент), представьте, что вы меняете компонент на другой - отображающий ваши данные как-то иначе. В идеале, модель (композабл) должен остаться тем же самым, вы переписываете только компонент, в котором логика, ответственная только за отображение.

В то же время, логика (Controller) может делиться между компонентом и композаблом. Например, валидация данных формы может происходить в компоненте (проверка, что поле заполнено), в компоненте с помощью сторонней утилиты (проверка, что пароль - "сложный") и в композабле (проверка, что username - уникален)