Несколько простых, но полезных советов по работе с геттерами в Vuex

    Vuex предоставляет удобные инструменты для работы с данными, но некоторые разработчики используют их не всегда по назначению, либо создают избыточные конструкции там, где можно было написать более понятно и ёмко, такое также случается, когда разработчик только знакомится с данными инструментами. В данной статье будут приведены некоторые рекомендации по организации геттеров (Getters), которые вы сможете применить в работе.


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


    Использование геттеров для простого получения данных из хранилища


    Давайте разберем простой пример кода:


    state: {
      films: [
        { id: 1, name: 'Джеймс Бонд' },
        { id: 2, name: 'Гарри Поттер' },
        { id: 3, name: 'Автострада 60' },
      ],
    },
    getters: {
      films: state => state.films,
    },

    Такое использование геттеров встречается достаточно часто и это плохо. Для доступа к состоянию в вашем компоненте достаточно сделать вычисляемое значение в computed, например:


    computed: {
      films() {
        return this.$store.state.films;
      },
    },

    Либо еще более удобный вариант с использованием mapState:


    computed: {
      ...mapState(['films']),
    },

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


    ИМХО: многие миксуют использование mapState и создание вычисляемых значений, возвращающих состояние. Для создания единообразного кода используйте mapState и остальные инструменты даже для одного значения, так как ваш код станет более единообразным и в него можно будет вносить изменения гораздо быстрее и удобнее, к примеру если придется вывести еще одно значение.


    Создавать геттер для единственного случая использования фильтра


    Предположим, что вам нужно получить фильм про Джеймса Бонда, для какого-то специфического случае, возможно вам захочется сделать так:


    getters: {
      bondFilm: state => state.films
        .find(f => f.name === 'Джеймс Бонд') || {},
    },

    Не нужно так поступать, лучше снова обратиться к mapState и сделать следующим образом:


    computed: {
      ...mapState({
        bondFilm: state => state.films
          .find(f => f.name === 'Джеймс Бонд') || {},
      }),
    },

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


    Создавать геттеры с параметрами


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


    getters: {
      filmById: state => id => state.films
        .find(film => film.id === id) || {},
    },

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


    getters: {
      filmsById: (state) => {
        const result = {};
        state.films.forEach((film) => {
          result[film.id] = film;
        });
        return result;
      },
    },

    В данном случае повторное вычисление произойдет только в случае изменения данных, а вы сможете обращаться к id, как к ключам объекта.


    Подведем небольшой итог


    • Не используйте геттеры для простого получения данных, не нужно усложнять свой код обертками для простых операций
    • Не создавайте геттреры для специфических фильтров, которые будет необходимы только однажды, основная идея геттеров — это получение производных состояний, но не нужно переносить логику компонента в логику хранилища
    • Параметризированные геттеры теряют своё основное свойство — кэшируемость, подумайте несколько раз, прежде чем пользоваться ими таким способом
    • Пользуйтесь всеми преимуществами mapState, помните, что большую часть специфических задач для компонента можно выполнить с его помощью
    • Используйте mapGetters, об этом было сказано косвенно, но единообразие лучше, чем его отсутствие

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

    Поделиться публикацией

    Комментарии 19

      +2
      > Такое использование геттеров встречается достаточно часто и это плохо.

      А почему плохо, я так и не понял. Я лично делаю геттер и вот почему: стэйт обычно нужен когда несколько компонентов работают с одними данными, и геттер лучше будет в одном месте а не в нескольких компонентах. Создаю его я даже если он только в одном уомпоненте используется, и отчасти не потому что «пусть будет, мало ли какой новый компонент его потребует», а потому что его сделал, добавил в юнит тест стораджа и забыл.
      ...mapGetter(['films']),

      выглядит точно так же красиво как
      ...mapState(['films']),


      Так что я не могу согласится с первыми 2 пунктами, по крайней мере пока не увижу реальный проблемы с ними.

      По третеьему пункту соглашусь. Геттеры с параметрами это боль. Особенно тестирование.
        +1
        Я думаю, что достаточно радикально выразил свои мысли. В данном случае имелось ввиду, что такое использование геттеров вызывает просто дублирование кода, чаще всего так и получается на практике. Но если такое использование геттеров удовлетворяет какие-то архитектурные запросы в проекте, то почему нет. По второму пункту, это мое личное мнение в плане переносимости логики. Например в моей практике есть примеры переиспользования бизнес логики из хранилища в других проектов, которые работают в рамках одной системы, поэтому выносить частное применение чего-либо в общее хранилище может быть неправильным. Например аналогичная ситуация происходит с экшенами, разработчики превращают их в место хранения обращений к апи, это тоже оверюзное использование стора. Опять таки, это рекомендации начинающим разработчикам, со временем люди нарабатывают свой опыт и уже сами находят удобное для себя использование.
          +1

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

            +1
            Я думаю одним из вариантов является создать api factory, например как тут link.medium.com/EtLkVqMC7X. Такой вариант позволяет отделить генератор апи обращений и их хранение. Затем вы просто обращается к ним из нужного вам место. Просто нужно помнить, что если апи не является порождающим глобальные изменения и его вызов нужен только единожды для какого-то простого действия, то не выносите его в стор, точно так же, как не нужно тащить в стор флаги заугрузки, да, это удобно, но всё таки это логика компонента.
              +1
              Соглашусь, что в случае необходимости данных только внутри компонента, хранить эти данные (а если их еще и мутировать надо — то это еще и дополнительные экшены/мутации) в сторе — это оверхед. У меня реализация примерно такая же, как вы и предложили, единственное что у меня не фабрика, а файл с набором методов-промисов, которые я импортирую в стор если это нужно хранить глобально, либо, если это «локальные нужды» компонента, то импортирую метод прямо в компонент и там работаю с результатом. Надо будет задуматься над преимуществами реализации в виде фабрики…

              С флагами тоже поддерживаю, а ведь именно так раньше я и делал. Управление флагами загрузки оказалось гораздо более гибким и очевидным если оно реализовано внутри компонента, а не стора.
            0
            А можете объяснить зачем вообще использовать гетеры или компютед методы когда и одно и другое скорее для формата данных или какой то постобработки, а просто пробрасывать лишнее действие, операция чтения из стейта безопасна и можно использовать то что нужно прямо в шаблоне.
            <li v-for="(film,index) in $store.state.films" :key=index>
                {{film.title}}
            </li>
            
              +1
              Ну тут скорее вопрос аккуратности написанного кода. Когда я вижу в коде такое обращение к state — это не очень удобно, так как скорее всего для сложного фильтра у вас mapState или геттер в компьтед, а для простого вы дергаете в темплейте $store.state.films, то это выглядит громоздко, особенно если нас в какой-то момент понадобится отфильтровать вывод по какому-то параметру, то придётся вносить гораздо больше изменений.
                0
                В вашем примере это можно использовать и так, как вы написали, разве что страдает чистота кода. Но если пример будет более сложный и геттер используется в нескольких компонентах, то при смене структуры хранилища, нужно будет изменить путь только в одном геттере, а не искать все компоненты, в которых путь был «прибит гвоздями». Тестирование так же усложнится.
            0
            А что если геттер для получения свойства из state используется в разных компонентах? Каждый раз делать computed property? Мне кажется что всё-таки сделать его глобальным будет лучше в таком случае.
              0
              Вы точно также можете использовать само свойство, оо есть по сути вы будете в каждом компоненте работать с mapState. Либо делать return this.$store.state.stateName, что не очень красиво. Когда вы делаете геттер, то накладываете ещё один слой кода, делать это для простого действия не очень хорошо, это размазывает логику по коду, особенно если вы храните state, mutations, actions и getters в разных файлах.
                0
                Спасибо за пояснение. Просто в моем случае во vuex лежат только глобальные данные (после инициализации приложения, например имя пользователя) и свойства нужно дергать одинаково в очень многих местах. На мой взгляд удобнее иметь глобальный геттер в этом случае, чем делать 10 совершенно идентичных computed property в разных местах.

                Или в этом случае кошернее будет обращаться к $store.state.* напрямую?
                  0
                  Я так понимаю у вас геттер, который собирает имя пользователя мз двух полей, то да, ну или он возвращает user.name. Но если вы в сторе держите напрямую поле userName, то лучше делать ...mapState(‘userName’). Разработчики, которые будут поддерживать код после вас скажут вам спасибо)
                    0
                    Но если вы в сторе держите напрямую поле userName, то лучше делать ...mapState(‘userName’). Разработчики, которые будут поддерживать код после вас скажут вам спасибо)

                    Вы снова не ответили на вопрос чем это лучше.

                    А если понадобится поменять имя поля? Искать все кейсы с mapstate или один раз в одном геттере изменить.
                      0
                      Если честно, причина достаточно сомнительная, так как если у вас в проекте происходит такое глобальное изменение, то скорее всего имя геттера тоже должно быть застронуто. Если вы условно изменили userName на nickName, но геттер остался с тем же именем, то это вызовет как минимум недоумение у других разработчиков.
                    0
                    У меня, например, в сторе хранится пользователь с полем, которое описывает его права:
                    user: {
                      roles: ['IS_ADMIN', 'IS_COMPANY', 'IS_USER'],
                      ...
                    }

                    В сторе у меня есть глобальный геттер, который определяет, является ли пользователь админом:
                    isAdmin() {
                      return this.state.roles.includes(Roles.ROLE_ADMIN);
                    }
                +1
                    .reduce((result, film) => ({
                      ...result,
                      [film.id]: film,
                    }), {})

                Вместо простого алгоритма наполнения хэша со сложностью O(n) получается n наполнений хэша общей сложностью O(n!). Создается n промежуточных объектов. В некоторых случаях это может создать большую нагрузку на GC, т.к. общее количество ключей во всех этих объектах тоже будет n!..
                Такое будет работать быстро в функциональных языках, благодаря оптимизациям, которые возможны из-за других особенностей языка. В других языках, включая js, этих оптимизаций может не быть (скорее всего).
                Подходы ФП могут сделать код читаемым и лаконичным. Хотя в этом примере страдает даже читаемость.

                  +1
                  Соглашусь с вами.
                  getters: {
                    filmsById: (state) => {
                      const result = {};
                      state.films.forEach((film) => {
                        result[film.id] = film;
                      });
                      return result;
                    },
                  },
                  
                  0
                  const [film = {}] = state.films.filter(f => f.name === 'Джеймс Бонд');

                  Понятно, что это только пример, но все-таки правильнее использовать Array.find
                  const film = state.films.find(f => f.name === 'Джеймс Бонд') || {};
                  
                    0
                    Да, думаю так будет правильнее.

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

                  Самое читаемое