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 - уникален)