В этой статье пойдет речь об распространенной ошибке, которую делают большинство начинающих при разработке приложения на 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.
- Геттерам можно передавать дополнительные аргументы, чтобы проводить вычисления данных на их основе.
- Результаты геттера обновляются при изменении одной из зависимостей.
Документация по геттерам на русском
Комментарии (21)
CrocodileRed
17.02.2019 09:07
vvtvvtvvt
17.02.2019 10:27-1Интересная статья, тоже задавался этим вопросом. Но сейчас я всё же предпочитаю использовать getter в компонентах…
Раньше, я тоже тянул state на прямую из Vuex. Но сейчас, столкнулся с проблемой — формат данных часто меняется, как следствие меняется и формат state в Vuex, и в результате приходится вносить изменения в десятки компонент, которые пользовались этим state (которые надо ещё найти в проекте) делать это приходится в десятках файлов, и это не удобно.
По этому сейчас стараюсь все данные в компоненту загружать только через gettеr, что бы потом, в случае изменения формата данных, внести изменения только в соответствующий getter, тем самым не меняя интерфейс через который компонента взаимодействует с хранилищем. Поначалу такие геттеры могут показаться излишними, но в перспективе, при постоянном изменении формата данных, мне кажется они себя оправдывают…CrocodileRed
17.02.2019 11:29Ситуация, описанная вами, кажется мне дикой. Вы пытаетесь исправить ошибки проектирования локальным кодом и это рано или поздно приведет к цугундеру. Ну и, кмк, вышеописанное является отличным опусом на тему "Как не нужно использовать геттеры".
vvtvvtvvt
17.02.2019 14:10В моём понимании, правила взаимодействия между компонетами и храниличем это тоже важная часть архитектуры. В нашем случае роль представления — вьюхи, играют компоненты Vue, а роль хранилича данных это state Vuex. Из архитектурных патерно MV*, мы знаем, что прямое обращение к храниличу данных из уровня представления — это не лучшая идея( как раз по причине их возможного изменения), лучше обращаться к ним через какую нибудь прослойку — например модель. Хоть vuex реализует flux, но проблема, как показывает опыт, никуда не дивается, прослойка нужна, и я пытаюсь обеспечить её через getter, снижая тем самым связанность прилоежния. А как вы архитектурно решаете эту проблему?
CrocodileRed
17.02.2019 15:01+1Вы верно излагаете теорию, но на практике я вижу ошибку в том, что хранилище хранит не модели (если я верно все понял). XAHTEP26 ниже все очень хорошо расписал, на мой взгляд. Я для решения описанной вами проблемы ввожу в приложение понятие
DataProvider
— связующее звено между базой данных и приложением — там я и занимаюсь нормализацией.
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 в компонентах.roma219
17.02.2019 14:59Думаю речь шла не про изменение формата данных из API, а про периодическую необходимость менять структуру стейта какого-то модуля (что-то добавилось, что-то решили больше внутрь убрать).
XAHTEP26
17.02.2019 15:43Если я неправильно понял и имелось ввиду, то о чем вы говорите то дополню:
Если после изменения структуры хранилища те данные которые были не производными — стали производными, то надо добавить геттер для них и это не будет противоречить тому что написано в статье. Но не стоит добавлять геттеры для всех данных "по умолчанию".
На самом деле все просто. Эта и множество других, похожих как две капли воды, статей про геттеры Vuex пытаются донести одно простое правило:
Не надо делать такие геттеры:
getProp(state) { return state.prop; }
roma219
17.02.2019 12:25Сомнительная статья, и чем так сильно «перегружается» приложение от использования геттеров? Наоборот, при доступе в стейт только через геттеры связность уменьшается, тк компоненты напрямую в стейт никогда не ходят, и, как заметил комментатор выше, при изменении структуры стейта, нужно лишь в одном месте поменять геттер.
А вообще смахивает на перевод статьи с медиума годовой давности.XAHTEP26
17.02.2019 14:20Приложение перегружается тем, что это увеличивает объем кода. И на связность это никак не влияет, потому что такие геттеры попросту отдают состояние. В общем геттер вида:
getUser(state) { return state.user; }
Это просто лишний кусок кода и лишний вызов функции при работе приложения. Не то чтобы он сильно нагружает приложение, но и ценности никакой не имеет.
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 в) при изменении структуры не надо думать, есть ли какие-то места в приложении, обращающиеся к стейту напрямую, я всегда знаю что доступ к нему будет осуществляться только через заданный мной интерфейс — геттеры. Пробовал оба подхода и второй оставляет ощущение большей «чёткости».CrocodileRed
17.02.2019 15:07Мне кажется, речь шла об увеличении исполняемого кода. вместо чего-то типа
return this.$store.state.user
происходит что-то типа
return userGetter.call(this.$store.state)
UPD. Однако, я вижу проблему не в этом, а именно в «нецелевом» использовании getter'ов.
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 одновременно. А значит и в последнем случае — я не получу выгоды от вашего подхода.
XAHTEP26
17.02.2019 16:50Насчет увеличения связности...
При использовании mapState, вам надо знать о структуре данных в модуле ровно столько же, сколько и при использовании mapGetters. Если я не прав — приведите пример в котором это не так.
xPomaHx
17.02.2019 21:22Стоп, чтение стейта это безопасная с точки зрения реактивности операция, апдейт другое дело, зачем вообще писать, что то в компоненте когда мы можем напрямую брать данные из стейта в шаблоне.
<li v-for="(book, index) in this.$store.state.books.list" :key="index">{{book.title}}</li>
Гетеры нужны для другого, для случая когда нам нужны разные виды представления одного и того же состояния.
Djaler
В самом начале третьей главы создаёте геттер, но не используете его
Savetchuk Автор
Исправил, спасибо!
Shtucer
Улучшение "на лицо" или "налицо".