Vuex

Руководство по использованию Vuex.

Когда использовать?

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

Разделение бизнес-логики и интерфейса

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

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

Модульный подход

В зависимости от типа проекта, мы будем использовать или НЕ использовать модульный подход Vuex.

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

const store = new Vuex.Store({
  modules: {
    account: {
      namespaced: true,

      // содержимое модуля
      state: () => ({ ... }), // состояние модуля автоматически вложено и не зависит от опции пространства имён
      getters: {
        isAdmin () { ... }, // -> getters['account/isAdmin']
      },
      actions: {
        login () { ... }, // -> dispatch('account/login')
      },
      mutations: {
        login () { ... }, // -> commit('account/login')
      },

      // вложенные модули
      modules: {
        // наследует пространство имён из родительского модуля
        myPage: {
          state: () => ({ ... }),
          getters: {
            profile () { ... }, // -> getters['account/profile']
          },
        },

        // большая вложенность с собственным пространством имён
        posts: {
          namespaced: true,

          state: () => ({ ... }),
          getters: {
            popular () { ... }, // -> getters['account/posts/popular']
          },
        }
      }
    }
  }
});

Структура папок при модульном подходе

  • store

    • modules

      • user

        • actions.js

        • getters.js

        • mutations.js

        • index.js

        • state.js

    • actions.js

    • getters.js

    • mutations.js

    • index.js

    • state.js

Причём если у модуля есть свои модули, то вложенность идёт дальше по модулям в соответствии с корнем (как в примере сверху с постами пользователя).

import Vue from 'vue';
import Vuex from 'vuex';
import state from './state.js';
import actions from './actions.js';
import mutations from './mutations.js';
import getters from './getters.js';
import user from './modules/user';

Vue.use(Vuex);

export default new Vuex.Store({
    state,
    mutations,
    actions,
    getters,
    modules: {
        user,
    },
});

state

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

Данные из state могут использоваться для чтения в любом месте Vue приложения.

Старайтесь избегать мутаций свойств объектов из state. Нам нужно наиболее предсказуемое поведение приложений.

const store = new Vuex.Store({
  state: () => ({
    // обязательно snake_case
    user: {...},
    csrf: "...",
  }),
});

getters

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

const store = new Vuex.Store({
  getters: {
    // обязательно camelCase
    someGetter (state) {
      return state.user;
    },
    
    // getter с доп. фильтром извне
    someFilteredGetter: (state) => (is_active) => {
      return state.users.filter((user) => user.is_active === is_active );
    },
  },
});

mutations

const store = new Vuex.Store({
  mutations: {
    // обязательно UPPERCASE + snake_case
    SET_POSTS: (state, posts) => {
      state.posts = posts;
    },
  },
});

Вторым параметром мы никогда не передаём название как payload. Мы должны сразу понимать что за сущность мы принимаем в мутацию. Мы также не используем Object-Style Commit.

Использование Mutation Types

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

// mutation-types.js
export const SET_POSTS = 'SET_POSTS';
export const SOME_MUTATION = 'SOME_MUTATION';
// actions.js
import * as types from './mutation_types';

commit(types.SET_POSTS, posts);
commit(types.SOME_MUTATION, some_data);
// mutations.js
import * as types from './mutation_types';

export default {
  [types.SET_POSTS](state, posts) {
    state.posts = posts;
  },
  
  [types.SOME_MUTATION](state, some_data) {
    state.some_data = some_data;
  },
};

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

Мутации должны быть синхронными

Следует помнить одно важное правило: функции обработчика мутаций должны быть синхронными. Почему? Рассмотрим следующий пример:

mutations: {
  someMutation (state) {
    api.callAsyncMethod(() => {
      state.count++
    })
  }
}

Теперь представьте, что мы отлаживаем приложение и просматриваем журналы изменений devtool. Для каждой зарегистрированной мутации инструменту разработки необходимо будет делать снимки состояния «до» и «после». Однако асинхронный обратный вызов внутри приведенного выше примера мутации делает это невозможным: обратный вызов еще не вызывается, когда мутация зафиксирована, и инструмент разработки не может узнать, когда на самом деле будет вызван обратный вызов — любая мутация состояния, выполненная в обратном вызове. по существу не отслеживается!

actions

Когда мы используем какой-либо action внутри компонентов, все необходимые мутации должны происходить автоматически. Также мы должны иметь доступ к запрашиваемым данным.

При использовании Promise(), всегда прокидывайте также reject(err)

// плохо
actions: {
    fetchPosts() {
        return new Promise((resolve, reject) => {
            api.get_posts()
            .then(({ data }) => resolve(data.posts))
            .catch((err) => reject(err))
        });
    }
}

this.$store.dispatch('fetchPosts')
    .then((posts) => {
        this.$store.commit('SET_POSTS', posts);
    });


// хорошо
actions: {
    fetchPosts({ commit }) {
        return new Promise((resolve, reject) => {
            api.get_posts()
            .then(({ data }) => {
                commit('SET_POSTS', data.posts);
                resolve(data.posts);
            })
            .catch((err) => reject(err))
        });
    }
}

this.$store.dispatch('fetchPosts');

helper-функции

Функции mapGetters, mapActions, mapMutations, mapState - запрещено использовать.

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

  • this.$store.state.some_state

  • this.$store.getters['someGetter']

  • this.$store.dispatch('someAction')

  • this.$store.commit('SOME_MUTATION', args)

Last updated