Если спустя время у вас происходило такое, что компонент-стор разрастался, а каждое добавление или исправление логики уже пугало, то эта статья для вас.

Сегодня я не буду читать лекций. Просто поделюсь тем, как сам дошел до более-менее устойчивого подхода после того, как неоднократно рефакторил чужие сторы и ловил себя на мысли: «Зачем я это трогал?». Возможно, где-то я буду неправ, возможно, в вашем проекте это не взлетит. Я не напрашиваюсь в учителя, просто фиксирую опыт, чтобы не наступать на одни и те же грабли.

1. Не мутируйте состояние напрямую из компонентов

Встречал код примерно такого вида:

const docStore = useDocumentStore()
const { client } = storeToRefs(docStore)
client.value = null // Боль

Проблема не в том, что это сломается сразу. Проблема в том, что через полгода вы будете искать, кто и почему обнулил client, а логика изменения размазана по пяти компонентам.

Правильнее вынести мутации в экшены самого стора:

// в store
export const useDocumentStore = defineStore('documents', () => {
  const client = ref(null)
  const resetClient = () => { client.value = null }
  return { client, resetClient }
})

// в компоненте
docStore.resetClient()

Так у вас всегда одна точка входа. Если завтра добавится логирование, валидация или отправка события в аналитику - вы поменяете только один метод, а не будете бегать по всему проекту.

2. Используйте Setup Stores

Options API для сторов остался в прошлом. defineStore(() => { ... }) даёт всё, что нужно: прозрачную реактивность, возможность выносить хуки в отдельные композаблы и, что важно, гораздо проще тестируется.

Если вы до сих пор пишете state: () => ({ ... }), actions: { ... } - попробуйте переписать хотя бы один новый стор. Разница заметна сразу: меньше шаблонного кода, нет this, реактивность работает «как в компонентах».

3. Забудьте про watch внутри сторов

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

В 90% случаев вотч можно заменить методом, который явно вызывает компонент или другой стор. Если нужно реагировать на изменения роута, URL-параметры или внешние события - делайте это в компоненте или в отдельном композабле, а в стор просто передавайте готовые данные через экшен. Стор должен быть «тупым» хранилищем, а не диспетчером событий.

4. localStorage - только через плагин или обёртку

Если стору нужно переживать перезагрузку страницы, не пишите localStorage.setItem в каждом экшене.

Либо используйте проверенный pinia-plugin-persistedstate, либо напишите свою обёртку с чётким интерфейсом и тестами. Главное - чтобы логика синхронизации не перемешивалась с бизнес-логикой.

5. Стор != свалка логики

Это, пожалуй, самый важный пункт. Стор должен заниматься только одним: читать, сохранять и обновлять данные. Не нужно тащить туда подписки на роутер, парсинг токенов, работу с веб-сокетами или расчёты, которые зависят от трёх других сторов.

Если логика начинает усложняться - выносите её. Стор останется чистым, его будет легко мокать в тестах, а рефакторинг перестанет вызывать панику.

Простой чек-лист перед коммитом: «Могу ли я заменить этот стор на обычный файл с константами и функциями, не сломав архитектуру?» Если ответ «нет» - скорее всего, вы перегрузили его ответственностью.

Итог

Идеального стора не существует. Есть только тот, который не вызывает желания удалить его при следующем открытии IDE. Держите их маленькими, явно описывайте контракты экшенов и не бойтесь выносить сложность наружу. Со временем это превращается в привычку, а проект начинает дышать.

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

Комментарии (27)


  1. Vadiok
    07.04.2026 05:50

    Ещё для меня очень странно, когда стор используют для получения каких-то сущностей с бэка (например, товаров), прописывают там логику с fetch и т.д., а потом используют это только в одном компоненте.

    Такая практика часто встречается в различных туториалах по Vue.


    1. iprs
      07.04.2026 05:50

      Вполне нормальный подход. Считаем стор единственным местом хранения данных. Все взаимодействие с бэком - скрыто внутри стора. Естественно, это не значит, что все нужно запихать в один файл, где объявлен этот самый стор.


      1. Vadiok
        07.04.2026 05:50

        А какие плюсы-то у такой логики?

        Минусы вот следующие:

        • Если данные нужны только в 1 компоненте, то логика получается размазана между стором и компонентом, хотя логично было бы хранить ее рядом с компонентом, которому она нужна

        • Если список тех же сущностей понадобится другому компоненту, но уже с другими фильтрами, сортировкой и т.п., то придется дублировать стор.

        • Стор перестает быть тупым и начинает отвечать за запросы и их состояние, вместо того, чтобы просто сохранять и отдавать.


        1. Pubert
          07.04.2026 05:50

          Внутри стора нельзя писать логику получения запросов. Для этого есть сервисы. Сервис должен получать данные с АПИ, обрабатывать и отправлять в стор

          Зачем хранить данные в сторе, если они используются в 1 компоненте? 1) консистентность - данные с Бека хранятся в одном месте; 2) удобство тестов - можно легко мокать стор (попробуйте замокать локальную переменную где-то в сервисе); 3) удобная отладка - просмотр содержимого стора; ...и т.д.


          1. Vadiok
            07.04.2026 05:50

            Внутри стора нельзя писать логику получения запросов. Для этого есть сервисы.

            Т.е. по вашей логике когда компоненту надо что-то получить с бэка, он дергает сервис, а результат берет из стора? Или все таки он запрос делает к стору и стор берет данные из сервиса, но тогда уже получается не сильно важно стор делает fetch(...) или что-то типа userService.fetchList(...).

            консистентность - данные с Бека хранятся в одном месте

            Так в том и проблема, что в случае списков не всем компонентам нужна консистентность. Например одному компоненту нужны товары с сортировкой по цене по 20 на страницу. Сделали с хранением в сторе. А теперь задача возникла, что другой компонент должен отображать те же сущности товаров, но самые новые, и их всего 5. Для этого придется в сторе заводить еще список, консистентность тут наоборот создает проблему.

            удобство тестов - можно легко мокать стор (попробуйте замокать локальную переменную где-то в сервисе)

            Так тестируйте сервис.

            удобная отладка - просмотр содержимого стора

            Содержимое компонента тоже удобно отлаживается. И самое главное, в его внутреннее содержимое ничего не попадет из того, что он сам не запросит. А если несколько компонентов могут менять стор, то в некоторых случаях еще отлови попробуй, какой из них его поменял.


          1. iprs
            07.04.2026 05:50

            Внутри стора нельзя писать логику получения запросов.

            А авторы pinia в своей документации утверждают, что можно и нужно. И я им верю больше.


        1. iprs
          07.04.2026 05:50

          Ну, например, чтобы после вызова метода стора, он сам выполнил запрос к серверу, получил ответ, и обновил внутри себя связанные данные. А иначе, придется колхозить какие-нибудь шины событий или еще что.

          Например. Есть форум. В нем есть обычная модалка для редактирования профиля пользователя. Поменял он аватарку. Сразу после изменения она должна обновиться везде, где отображается (во всех отображаемых сообщениях от него).

          И кто вообще сказал, что стор должен обслуживать только один компонент? И что плохого в том, что он умный?


          1. Vadiok
            07.04.2026 05:50

            Например. Есть форум. В нем есть обычная модалка для редактирования профиля пользователя...

            Я не писал про профиль пользователя, я писал про списки сущностей, которые можно получать в т.ч. с пагинацией, фильтрами, сортировкой и т.д..

            кто вообще сказал, что стор должен обслуживать только один компонент

            И кто, простите, такое сказал? Точно не я.

            что плохого в том, что он умный?

            Прочитайте про God-классы, чем они плохи. Примерно тем же плохи и умные сторы и все прочее. Комментарием выше @Pubert, также споря со мной, при этом пишет:

            Внутри стора нельзя писать логику получения запросов...


            1. iprs
              07.04.2026 05:50

              То есть, некий класс, который внутри себя использует стор и сервис взаимодействия с сервером (2 зависимости) - это хорошо?

              А стор, который использует внутри себя сервис взаимодействия с сервером (1 зависимость) - это плохо?

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

              Хотя могу предположить, что у вас просто не было по настоящему сложных хранимых на клиентской стороне взаимозависимых структур данных, изменения части которых вызывают цепочки изменений в других данных. Или вы в таких случаях снова загружаете абсолютно все с сервера, даже если требуется поменять пару полей данных?


              1. Vadiok
                07.04.2026 05:50

                Я не понимаю контекст вашего ответа, явно вы отвечаете не на мои комментарии.

                То есть, некий класс, который внутри себя использует стор и сервис взаимодействия с сервером (2 зависимости) - это хорошо?

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

                А стор, который использует внутри себя сервис взаимодействия с сервером (1 зависимость) - это плохо?... S из solid не нарушается.

                У стора S - это хранение данных, ну и плюсом идет каким-то образом их изменение. Когда он отвечает за запросы к серверу - это уже нарушение Single Responsibility Principle.

                Хотя могу предположить, что у вас просто не было по настоящему сложных хранимых на клиентской стороне взаимозависимых структур данных

                Да, откуда мне их взять-то, железная логика. Перечитайте мои комменты, про что я писал. Лень дублировать.


                1. iprs
                  07.04.2026 05:50

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

                  Ну то есть у вас реально не было задач, когда одни и те же данные должны использоваться несколькими компонентами?

                  У стора S - это хранение данных, ну и плюсом идет каким-то образом их изменение.

                  Actions are the equivalent of methods in components. They can be defined with the actions property in defineStore() and they are perfect to define business logic:

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


                1. iprs
                  07.04.2026 05:50

                  нарушение Single Responsibility Principle

                  Паттерн Active Record тоже его формально нарушает, при этом применяется в каждом втором проекте для веба


                  1. Vadiok
                    07.04.2026 05:50

                    Отвечу сразу на оба ваших комментария.

                    Ну то есть у вас реально не было задач, когда одни и те же данные должны использоваться несколькими компонентами?

                    Прочитайте, о чем я пишу. Я пишу о списках сущностей, которые используются в одном компоненте. Я нигде не писал, что стор не нужен для общих данных!!!!!!!!!!!!!!!!!!!

                    Actions ... are perfect to define business logic:

                    Тут обсуждаемая статья как раз про то, что использовать формат Option Store - устаревший формат. И я с автором соглашусь.

                    И не должны компоненты знать ничего о способе общения с сервером.

                    Как и сторы, для этого отдельные сервисы используются.

                    А уже внутри этого метода может быть как общая бизнес-логика...

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

                    Паттерн Active Record тоже его формально нарушает, при этом применяется в каждом втором проекте для веба

                    Но при этом и проблем приносит, не зря в крупных проектах с данными работают чаще иным образом.


                    1. iprs
                      07.04.2026 05:50

                      Тут обсуждаемая статья как раз про то, что использовать формат Option Store - устаревший формат. И я с автором соглашусь.

                      Причем тут option? Это же просто синтаксический сахар, преобразуемый на этапе сборки в синтаксис composition. Я привел доказательство того, что методы в сторе как раз позиционируются как место для бизнес-логики самими авторами.

                      Наличие actions никак не зависит от синтаксиса, это одна из базовых возможностей pinia.

                      но все-таки лучше для этого использовать отдельные сервисы

                      Минус в том, что бизнес-логика разъезжается по разным файлам, и новый разработчик (или старый, после перерыва) легко может полезть в стор напрямую. Или наоборот забыть что то поменять в нем после общения с бэком


        1. iprs
          07.04.2026 05:50

          Если список тех же сущностей понадобится другому компоненту, но уже с другими фильтрами, сортировкой и т.п., то придется дублировать стор.

          Зачем? Фильтруйте и сортируйте внутри компонента (computed или вызов метода), а данные должны лежать в сторе, и не меняться


          1. Vadiok
            07.04.2026 05:50

            Т.е. вы предлагаете несколько тысяч товаров загрузить клиенту за 1 запрос (а может и десятков тысяч), а потом браузером работать с этим списком?


            1. iprs
              07.04.2026 05:50

              В таком случае стор вообще не нужен )

              Грузите сразу из сервиса в компонент, раз кроме него, они нигде не требуются.


              1. Vadiok
                07.04.2026 05:50

                Мой изначальный коммент:

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

                Вы отвечаете:

                Вполне нормальный подход. Считаем стор единственным местом хранения данных. Все взаимодействие с бэком - скрыто внутри стора

                Теперь вы же:

                В таком случае стор вообще не нужен )

                Грузите сразу из сервиса в компонент, раз кроме него, они нигде не требуются.


                1. iprs
                  07.04.2026 05:50

                  Десятки тысяч сущностей конечно хранить на клиенте не надо. В начале обсуждения такого условия не ставилось. А оно принципиальное.

                  А вот со списком товаров не совсем понятно. Если нет задачи отображать на экране несколько списков сразу - то я не понимаю, зачем вы пишите про дублирование списков внутри стора? Пусть будет один массив "отображаемые товары". И он бы изменялся с помощью вызова методов, внутри которых выполнялась загрузка данных. Или, если вам так важно, при вызове метода в сервисе, который загрузит данные а потом вызовет метод стора, и скопирует их в него.


                  1. Vadiok
                    07.04.2026 05:50

                    В начале обсуждения такого условия не ставилось. А оно принципиальное.

                    Так и в жизни так же, сначала такое условие не ставится, а потом может и поставиться.

                    Т.е., допустим, у нас есть сторы сущностей user, product, orders. Мы по вашей логике храним эти сущности в сторах, через сторы запрашиваем.

                    Далее пришла задача получать не только основной список products и отображать его, но одновременно с этим отображать список поменьше - featured products.

                    Тут мы либо переносим логику получения товаров в компоненты, у каждого свой список, но тогда архитектура приложения в плане работы со списками будут разной - user, orders - хранятся в сторе, а логика работы с products переносится в компоненты. Либо мы в сторе для product имеем 2 списка и загружаем их двумя методами. Но зачем это делать, если каждый список нужен своему компоненту?

                    А потом еще прилетит задача убрать компонент featured products - удалим ли мы список получения товаров для него из стора или забудем и тогда он останется там до тех пор, пока кто-то за глобальный рефакторинг не возьмется?

                    Если нет задачи отображать на экране несколько списков сразу - то я не понимаю, зачем вы пишите про дублирование списков внутри стора

                    Так задачи может изначально не быть, потом она может появиться, не лучше ли такой момент предусмотреть и сразу не использовать такую архитектуру?

                    Или, если вам так важно, при вызове метода в сервисе, который загрузит данные а потом вызовет метод стора, и скопирует их в него.

                    Тут получается такая странная штука:

                    • Компонент вызывает сервис

                    • Сервис кладет данные в стор

                    • Компонент берет данные из стора

                    Т.е. запрос для получения идет в сервис, а данные приходит из стора. Логичнее было бы:

                    • Компонент просит данные у сервиса

                    • Сервис откуда-то их берет (бэк, кэш, его дело) и отдает компоненту.


                    1. iprs
                      07.04.2026 05:50

                      Я бы не делал отдельный стор для каждой сущности. Собственно, я изначально это и имел в виду, как преимущество единого стора на все приложение (ну, как минимум для тесно связанных сущностей). В данном случае, навскидку, я бы сделал два: UserStore и ProductStore.

                      Соответственно, после аутентификации загружаем данные пользователя вместе с его историей заказов в userStore. Или изначально без истории заказов, но загружаем ее при первом обращении.

                      Точно так же создаем новый заказ, вызовом метода (экшна) у стора пользователя userStore.createOrder(productList);

                      Далее пришла задача получать не только основной список products и отображать его, но одновременно с этим отображать список поменьше - featured products.

                      Можно внутри productStore сделать переменную типа Map, где ключ - тип списка, а значение - собственно список. Тогда можно вообще не волноваться за появление/удаление вариантов списка товаров. Новый тип списка будет создан компонентом, нуждающемся в нем

                      Т.е. запрос для получения идет в сервис, а данные приходит из стора. Логичнее было бы Сервис откуда-то их берет (бэк, кэш, его дело) и отдает компоненту.

                      И эти данные потеряли реактивность


                      1. Vadiok
                        07.04.2026 05:50

                        Ок, я понял вашу логику. Процитирую ваш ответ здесь же другому пользователю:

                        > Внутри стора нельзя писать логику получения запросов.

                        А авторы pinia в своей документации утверждают, что можно и нужно. И я им верю больше.

                        Если вы верите, то вера - это уже не дискуссионный вопрос. Просто вот в этом моменте мы с вами не согласимся.

                        Для меня что бы авторы Pinia не утверждали, я беру их библиотеку, чтобы во Vue приложении у меня было общее реактивное хранилище.

                        Если оно мне не нужно как общее, а нужно в каком-то одном месте, то я считаю, что и логика работы должна быть описана где-то рядом с местом использования. Т.е. за разбивание логики работы приложения сначала по бизнес-фичам, а уже потом по функционалу.

                        Можно внутри productStore сделать переменную типа Map, где ключ - тип списка, а значение - собственно список

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

                        Единствнный вопрос - вы бы для этой переменной типа Map использовали ref или shallowRef? И вообще в сторах вы списки и объекты в каком виде храните - в виде ref или shallowRef?


                      1. iprs
                        07.04.2026 05:50

                        Единствнный вопрос - вы бы для этой переменной типа Map использовали ref или shallowRef? И вообще в сторах вы списки и объекты в каком виде храните - в виде ref или shallowRef?

                        Для этой задачи прмименил бы ref, т.к. с большой вероятностью нужно отслеживать изменения на всем уровне вложенности

                        А так, смотря что требуется. Иногда достаточно shallowRef, если меняется весь объект целиком, а не отдельные поля (элементы массива)


                      1. Vadiok
                        07.04.2026 05:50

                        Для этой задачи прмименил бы ref

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


                      1. iprs
                        07.04.2026 05:50

                        А при использовании shallowRef я вообще никакие изменения не смогу отследить. Map создается вместе со store и более не меняется.

                        А списки то как раз будут меняться - при пагинации, смене фильтров на странице, и тому подобного


                      1. Vadiok
                        07.04.2026 05:50

                        использовании shallowRef я вообще никакие изменения не смогу отследить

                        Зачем же его придумали тогда?

                        const products = shallowRef(new Map());
                        products.value.set('someKey', someArray);
                        // Триггерим изменение
                        triggerRef(products);
                        // или
                        products.value = products.value;

                        Map создается вместе со store и более не меняется, а списки то как раз будут меняться - при пагинации, смене фильтров на странице, и тому подобного

                        Меняться будет только весь список целиком, а не ключи каждого товара, а вы c ref реактивность вешаете на каждый ключ каждого товара, хотя с бэка вообще может прилететь гораздо больше ключей, чем на фронте надо. В итоге память на хранение таких данных увеличивается в разы.


                      1. iprs
                        07.04.2026 05:50

                        Зачем же его придумали тогда?

                        Для этого и придумали - чтобы отслеживать явное присвоение значения свойству value и ничего кроме этого

                        Меняться будет только весь список целиком, а не ключи каждого товара

                        прмименил бы ref, т.к. с большой вероятностью нужно отслеживать изменения на всем уровне вложенности

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

                        (Пример - в списке товаров приходит базовая цена, но при заказе нескольких экземпляров к ней должна применяться скидка. Хотя глупый пример получился. Ладно, у меня рабочий день закончился. Спасибо за дискуссию)