В этой статье пойдет речь об распространенной ошибке, которую делают большинство начинающих при разработке приложения на Vue + Vuex. Мы поговорим о геттерах (getters) и как их правильно использовать. Также мы рассмотрим вспомогательные функции mapState и mapGetters.


Примечания перед прочтением: рекомендуется иметь базовые знания Vue и Vuex.


Глава 1. Что такое геттеры. Пример нецелесообразного использования


Геттеры — это часть хранилища Vuex, которые возвращают вычисляемые данные текущего состояния хранилища нашим компонентам.


Рассмотрим пример:


const store = new Vuex.Store({
    state: {
        // список книг
        books: [
            { id: 1, title: '...', finished: true },
            { id: 2, title: '...', finished: false }
        ]
    },
    getters: {
        // возвращаем список всех книг
        books: state => state.books
    }
});

Так будет выглядеть компонент со списком всех книг:


export default {
    computed: {
        // наш геттер
        books() {
            return this.$store.getters.books
        }
    }
}

Пример выше работает, но так делать не стоит. Этим подходом мы перегружаем приложение.


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


Глава 2. Использование mapState для получения данных из хранилища


Документация гласит:


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

Давайте вернемся к нашему компоненту и используем mapState вместо геттера:


import { mapState } from 'vuex';

export default {
    computed: {
        ...mapState([
            'books'
        ])
    }
}

Геттер из хранилища можно удалить, т.к. мы в нем больше не нуждаемся:


const store = new Vuex.Store({
    state: {
        // список книг
        books: [
            { id: 1, title: '...', finished: true },
            { id: 2, title: '...', finished: false }
        ]
    }
});

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


Глава 3. Зачем же нужны геттеры, если есть mapState


И все таки они нужны. Геттеры используются в тех случаях, когда нужно вывести модифицированную информацию из хранилища (например список всех прочитанных книг).


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


const store = new Vuex.Store({
    state: {
        // список книг
        books: [
            { id: 1, title: '...', finished: true },
            { id: 2, title: '...', finished: false }
        ]
    },
    getters: {
        // возвращаем список прочитанных книг
        finishedBooks: state => {
            return state.books.filter(books => books.finished);
        }
    }
});

Теперь наш компонент будет выглядеть так:


import { mapState } from 'vuex';

export default {
    computed: {
        // получаем список всех книг
        ...mapState([
            'books'
        ]),
        // получаем список прочитанных книг
        finishedBooks() {
            return this.$store.getters.finishedBooks
        }
    }
}

На этом можно было бы остановиться, но есть еще одна полезная вещь, которую стоит знать. Если вам нужно переиспользовать один и тот же геттер в разных компонентах, может быть не совсем удобно прописывать геттеры каждый раз в методе computed. На помощь приходит mapGetters.


Давайте рассмотрим пример:


// не забываем про импорт
import { mapState, mapGetters } from 'vuex';

export default {
    computed: {
        // получаем список всех книг
        ...mapState([
            'books'
        ]),
        // получаем список геттеров, или в
        // нашем случае список всех прочитанных книг
        ...mapGetters([
            'finishedBooks'
        ])
    }
}

Улучшение налицо: использовав mapGetters мы сократили количество кода.


Вы так же можете вычислить информацию из хранилища основываясь на каких-то данных, например достать книгу по её id или названию. Этого можно добиться передав аргумент нашему геттеру.


const store = new Vuex.Store({
    state: {
        // список книг
        books: [
            { id: 1, title: '...', finished: true },
            { id: 2, title: '...', finished: false }
        ]
    },
    getters: {
        // возвращаем список прочитанных книг
        finishedBooks: state => {
            return state.books.filter(books => books.finished);
        },
        // возвращаем книгу по id
        getBookById: (state) => (id) => {
            return state.books.find(books => books.id === id)
        }
    }
});

import { mapState, mapGetters } from 'vuex';

export default {
    computed: {
        ...mapState([
            'books'
        ]),
        ...mapGetters([
            'finishedBooks',
            'getBookById'
        ]),
        getBook() {
            // получаем книгу с id === this.id
            return this.getBookById(this.id)
        }
    }
}

Закрепление материала


  • Используйте геттеры когда вам нужно вывести модифицированную информацию из хранилища (например список всех прочитанных книг), в иных случаях используйте вспомогательную функцию mapState.
  • Геттерам можно передавать дополнительные аргументы, чтобы проводить вычисления данных на их основе.
  • Результаты геттера обновляются при изменении одной из зависимостей.

Документация по геттерам на русском


Пример приложения из статьи на codepen

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


  1. Djaler
    16.02.2019 18:34

    В самом начале третьей главы создаёте геттер, но не используете его


    1. Savetchuk Автор
      16.02.2019 19:27

      Исправил, спасибо!


      1. Shtucer
        16.02.2019 20:43
        -1

        Улучшение "на лицо" или "налицо".



  1. andreyr82
    17.02.2019 09:17

    Пост для тех, кто не читал документацию?


  1. vvtvvtvvt
    17.02.2019 10:27
    -1

    Интересная статья, тоже задавался этим вопросом. Но сейчас я всё же предпочитаю использовать getter в компонентах…
    Раньше, я тоже тянул state на прямую из Vuex. Но сейчас, столкнулся с проблемой — формат данных часто меняется, как следствие меняется и формат state в Vuex, и в результате приходится вносить изменения в десятки компонент, которые пользовались этим state (которые надо ещё найти в проекте) делать это приходится в десятках файлов, и это не удобно.
    По этому сейчас стараюсь все данные в компоненту загружать только через gettеr, что бы потом, в случае изменения формата данных, внести изменения только в соответствующий getter, тем самым не меняя интерфейс через который компонента взаимодействует с хранилищем. Поначалу такие геттеры могут показаться излишними, но в перспективе, при постоянном изменении формата данных, мне кажется они себя оправдывают…


    1. CrocodileRed
      17.02.2019 11:29

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


      1. vvtvvtvvt
        17.02.2019 14:10

        В моём понимании, правила взаимодействия между компонетами и храниличем это тоже важная часть архитектуры. В нашем случае роль представления — вьюхи, играют компоненты Vue, а роль хранилича данных это state Vuex. Из архитектурных патерно MV*, мы знаем, что прямое обращение к храниличу данных из уровня представления — это не лучшая идея( как раз по причине их возможного изменения), лучше обращаться к ним через какую нибудь прослойку — например модель. Хоть vuex реализует flux, но проблема, как показывает опыт, никуда не дивается, прослойка нужна, и я пытаюсь обеспечить её через getter, снижая тем самым связанность прилоежния. А как вы архитектурно решаете эту проблему?


        1. CrocodileRed
          17.02.2019 15:01
          +1

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


    1. XAHTEP26
      17.02.2019 14:14

      Вы не правы.
      Если формат данных меняется, то надо приводить данные к нужному формату еще на входе.
      Например API возвращал объект в одном формате:


      let user = {
        "name": "Вася",
        "sex": "male",
        "image": "https://site.ru/img/vasya.png"
      };

      А потом стал возвращать в другом:


      let rawUser = {
        "login": "Вася",
        "male": true,
        "avatar": "/img/vasya.png"
      };

      То не надо пихать его в хранилище в таком виде в каком он пришел. Надо сразу же в обработчике результата запроса или в экшене или, на худой конец, в мутации привести объект к такому формату с которым ваше приложение привыкло работать:


      let user = {
        "name": rawUser.login,
        "sex": rawUser.male ? "male" : "female",
        "image": "https://site.ru" + rawUser.avatar
      };

      И только после этого сохранять его в состоянии.
      А откладывать преобразование формата на самый последний момент (в геттер) — это по прежнему не правильно. Геттеры нужны только для того чтобы вычислять производное состояние на основе состояния хранилища. Т. е. это практически то же самое, что computed в компонентах.


      1. roma219
        17.02.2019 14:59

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


        1. XAHTEP26
          17.02.2019 15:43

          Если я неправильно понял и имелось ввиду, то о чем вы говорите то дополню:


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


          На самом деле все просто. Эта и множество других, похожих как две капли воды, статей про геттеры Vuex пытаются донести одно простое правило:


          Не надо делать такие геттеры:


          getProp(state) {
            return state.prop;
          }


  1. roma219
    17.02.2019 12:25

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

    А вообще смахивает на перевод статьи с медиума годовой давности.


    1. XAHTEP26
      17.02.2019 14:20

      Приложение перегружается тем, что это увеличивает объем кода. И на связность это никак не влияет, потому что такие геттеры попросту отдают состояние. В общем геттер вида:


      getUser(state) {
        return state.user;
      }

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


      1. roma219
        17.02.2019 14:56

        Не согласен. А есть ли вообще увеличение объема кода, сравним:
        1)

        // модуль
        const getters = {
        ...
         getUser: state => state.user
        ...
        }
        
        // компонент
        ...mapGetters({
           user: 'module/getUser',
           // другие геттеры
        })
        


        2)
        {
           ...mapState({
             user: state => state.module.user,
           }),
           ...mapGetters({
           // другие геттеры
             ...
           })
        }
        


        Как видно, увеличение объема кода в случае 1 будет только в случае, когда другие геттеры не используются и надо писать mapGetters. Во всех остальных ситуациях же кода больше будет как раз таки в случае 2 из-за необходимости использовать и mapState, и mapGetters. Ну в целом по объему кода разницы большой нет, и это не критичный момент во всей этой ситуации на мой взгляд.

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

        Геттеры попросту отдают состояния и при использовании их извне мне не надо думать про структура стейта, а вот как раз через mapState мне каждый раз приходится задавать путь в стейте, т.е. компонент начинает что-то знать о структуре данных в модуле -> это и есть увеличение связности.

        Понятно, что ничего критичного в этой увеличенной связности нет, но все таки не могу согласиться с односторонним называнием «неправильным» подхода с использованием только геттеров. Лично я предпочитаю общение со стейтом только через геттеры, потому что: a) не надо думать о структуре стейта в компоненте б) не надо использовать и mapState и mapGetters в) при изменении структуры не надо думать, есть ли какие-то места в приложении, обращающиеся к стейту напрямую, я всегда знаю что доступ к нему будет осуществляться только через заданный мной интерфейс — геттеры. Пробовал оба подхода и второй оставляет ощущение большей «чёткости».


        1. CrocodileRed
          17.02.2019 15:07

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

          return this.$store.state.user

          происходит что-то типа

          return userGetter.call(this.$store.state)

          UPD. Однако, я вижу проблему не в этом, а именно в «нецелевом» использовании getter'ов.


        1. XAHTEP26
          17.02.2019 16:44

          Насчет увеличения объема кода...


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


          Но в ваших примерах вы приводите также код из компонента. Давайте рассмотрим несколько вариантов:


          Вариант 1:
          Допустим я не использую mapState и mapGetters. Тогда объем кода в компоненте для меня не меняется так как я в любом случае создаю вычисляемое свойство и в нем одной строкой возвращаю состояние хоть из стейта, хоть из геттера:


          // Без геттера users()
          users() {
            return this.$store.state.users;
          },
          
          // С геттером users()
          users() {
            return this.$store.getters.users;
          },

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


          Вариант 2:
          Допустим я использую mapState и mapGetters.


          // Без геттера users()
          ...mapState([
            'users'
          ]),
          
          // С геттером users()
          ...mapGetters([
            'users'
          ]),

          Снова не вижу разницы.


          Тут вы наверное заметите что я вас обманул, так как не использую и mapState, и mapGetters одновременно. Что ж — вы правы, если использовать их вместе, то код в компоненте увеличится:


          // Без геттера users()
          ...mapState([
            'users'
          ]),
          ...mapGetters([
            'articles',
            'comments'
          ]),
          
          // С геттером users()
          ...mapGetters([
            'users',
            'articles',
            'comments'
          ]),

          Но должен сказать что эти 2 лишние строчки скорее всего окупятся теми строками, которые вы не напишите в хранилище. И еще по своему опыту могу сказать следующее: так как я не пишу геттеры для каждого свойства в состоянии, то мне очень (очень очень) редко приходится использовать в компоненте mapState и mapGetters одновременно. А значит и в последнем случае — я не получу выгоды от вашего подхода.


        1. XAHTEP26
          17.02.2019 16:50

          Насчет увеличения связности...


          При использовании mapState, вам надо знать о структуре данных в модуле ровно столько же, сколько и при использовании mapGetters. Если я не прав — приведите пример в котором это не так.


  1. xPomaHx
    17.02.2019 21:22

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

    <li v-for="(book, index) in this.$store.state.books.list" :key="index">{{book.title}}</li>

    Гетеры нужны для другого, для случая когда нам нужны разные виды представления одного и того же состояния.


    1. Jenly
      18.02.2019 12:05
      +1

      <li v-for="(book, index) in this.$store.state.books.list" :key=«index»>{{book.title}}


      Пожалуйста, нет, не надо так


      1. xPomaHx
        18.02.2019 19:14

        Аргументы?