[Перевод] Какой будет новая версия Vuex?

Vuex — стейт менеджер для Vue приложений. Его следующая версия — Vuex 4, которая практически готова к официальному релизу. Она добавит поддержку Vue 3, но не принесет никакой новой функциональности.

Несмотря на то, что Vuex считается отличным решением и многие разработчики выбирают его как основную библиотеку для управления состоянием, они надеются получить больше возможностей в будущих релизах. Поэтому, пока Vuex 4 только готовится к выходу, один из его разработчиков, Kia King Ishii (входит в состав core-команды) уже делится планами для следующей, 5 версии. Стоит заметить, что это только планы и некоторые вещи могут измениться, тем не менее основное направление уже выбрано. О нем и пойдет речь.
С появлением Vue 3 и Сomposition API, разработчики стали создавать простые альтернативы. Например, в статье «Вероятно вам не нужен Vuex» демонстрируется простой, гибкий и надежный способ для создания сторов на основе Composition API совместно с provide/inject. Можно предположить, что этот и некоторые другие альтернативы вполне подойдут для небольших приложений, но как часто бывает, они имеют свои недостатки: документация, сообщество, соглашение в именовании, интеграция, инструменты разработчика.

3psapbo45lipr1l_jhi55sgtxo8.png

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

Создание стора


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

import { createStore } from 'vuex'

export const counterStore = createStore({
  state: {
    count: 0
  },
  
  getters: {
    double (state) {
      return state.count * 2
    }
  },
  
  mutations: {
    increment (state) {
      state.count++
    }
  },
  
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})


Стор все также состоит из 4 частей: состояние (state), где хранятся данные; геттеры (getters), предоставляющие вычисляемые состояния; мутации (mutations), необходимые для изменения состояния и экшены (actions), которые вызываются за пределами стора для выполнения операций над ним. Обычно экшены не просто вызывают мутацию (как в примере), а используются для выполнения асинхронных задач (потому что мутации должны быть синхронными) или реализуют какую-то более сложную логику. Как же будет выглядеть Vuex 5?

import { defineStore } from 'vuex'

export const counterStore = defineStore({
  name: 'counter',
  
  state() {
    return { count: 0 }
  },
  
  getters: {
    double () {
      return this.count * 2
    }
  },
  
  actions: {
    increment () {
      this.count++
    }
  }
})


Первое, что изменилось — переименование createStore в defineStore. Чуть позже будет понятно почему. Следующее, появился параметр name для указания имени стора. До этого, мы разделяли сторы на модули, а имена модулей были в виде именованных объектов. Далее модули регистрировались в глобальном пространстве, из-за чего они не были самодостаточными и готовыми для переиспользования. В качестве решения, нужно было использовать параметр namespaced, чтобы не давать нескольким модулям реагировать на тот же тип мутаций и действий. Думаю многие сталкивались с этим, но ссылку на документацию я, тем не менее, добавлю. Теперь у нас нет модулей, каждый стор по-умолчанию отдельное и независимое хранилище.

После указания имени, нам нужно сделать state функцией, которая возвращает начальное состояние, а не просто устанавливает его. Это очень похоже на то, как выглядит data в компонентах. Изменения коснулись и геттеров, вместо state как параметра функции мы используем this, чтобы получить доступ к данным. Тот же подход применен и к экшенам, this вместо statе как параметра. И наконец, самое главное, мутации объединены с экшенами. Kia отмечал, что мутации довольно часто становятся простыми сеттерами, делая их многословными, видимо это и послужило причиной удаления. Он не упоминает, можно ли будет производить изменение состояния за пределам стора, например из компонентов. Тут, мы можем только сослаться на Flux паттерн, который не рекомендует этого делать и поощряет подход с изменением состояния именно из экшенов.

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

import { ref, computed } from 'vue'
import { defineStore } from 'vuex'

export const counterStore = defineStore('counter', () => {
  const count = ref(0)

  const double = computed(() => count.value * 2)
  
  function increment () {
    count.value++
  }

  return { count, double, increment }  
})


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

Инициализация стора


Здесь нас ждут существенные изменения. Чтобы описать, как будет происходить инициализация стора в 5-ой версии, посмотрим как это происходит в 4-ой. Когда мы создаем стор через createStore, мы сразу же его инициализируем, чтобы затем использовать в app.use или напрямую.

import { createApp } from 'vue'
import App from './App.vue'
import store from './store'

const app = createApp(App)

app.use(store)
app.mount('#app')

// Теперь у всех компонентов есть доступ к `this.$store`
// Или к `useStore()` в контексте Composition API

import store from './store'

store.state.count // -> 0
store.commit('increment')
store.dispatch('increment')
store.getters.double // -> 4


В 5-ой версии, мы отдельно получаем доступ к каждому экземпляру Vuex, что дает гарантию независимости. Поэтому этот процесс выглядит иначе:

import { createApp } from 'vue'
import { createVuex } from 'vuex'
import App from './App.vue'

const app = createApp(App)
const vuex = createVuex()

app.use(vuex)
app.mount('#app')


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

import { defineComponent } from 'vue'
import store from './store'

export default defineComponent({
  name: 'App',

  computed: {
    counter () {
      return this.$vuex.store(store)
    }
  }
})


Вызов $vuex.store создает и инициализирует стор (помните про переименование createStore). Теперь, каждый раз общаясь к этому хранилищу через $vuex.store, вам будет возвращаться уже созданный экземпляр. В примере это this.counter, который мы можем использовать дальше в коде. Так же, можно инициализировать стор через createVuex().

И конечно, вариант для Composition API, где вместо $vuex.store используется useStore.

import { defineComponent } from 'vue'
import { useStore } from 'vuex'
import store from './store'

export default defineComponent({
  setup () {
    const counter = useStore(store)

    return { counter }
  }
})


У описанного выше подхода (инициализация стора через компоненты) существуют как преимущества, так и недостатки. С одной стороны это разделение кода и возможность добавлять его только там, где это необходимо. С другой — добавление зависимости (теперь нужно импортировать стор каждый раз, когда планируешь его использовать). Поэтому, если вы хотите использовать DI, то предлагается вариант с использованием provide:

import { createApp } from 'vue'
import { createVuex } from 'vuex'
import App from './App.vue'
import store from './store'

const app = createApp(App)
const vuex = createVuex()

app.use(vuex)
app.provide('name', store)
app.mount('#app')


И последующим пробрасыванием стора в компонент (здесь есть желание заменить name на константу и уже использовать ее):

import { defineComponent } from 'vue'

export default defineComponent({
  name: 'App',
  inject: ['name']
})

// Composition API

import { defineComponent, inject } from 'vue'

export default defineComponent({
  setup () {
    const store = inject('name')

    return { store }
  }
})


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

store.state.count            // State
store.getters.double         // Getters
store.commit('increment')    // Mutations
store.dispatch('increment')  // Actions


В новой версии ожидается:

store.count        // State
store.double       // Getters
store.increment()  // Actions


Все сущности (состояние, геттеры и экшены) доступны напрямую, делая работу с ними проще и логичнее. При этом убирается необходимость в использовании mapState, mapGetters, mapActions и mapMutations, а так же написания дополнительных вычисляемых свойств.

Совместное использование


Последние момент, который стоит рассмотреть — совместное использование. Мы помним, что во Vuex 5 у нас больше нет именованных модулей и каждый стор является отдельным и независимым. Это дает возможность импортировать их когда нужно и использовать данные по мере необходимости, прямо как компоненты. Появляется логичный вопрос, как использовать несколько сторов вместе? В 4-ой версии все еще существует глобальное пространство имен и нам нужно использовать rootGetters и rootState, чтобы обращаться к разным сторам в этой области (так же как и в 3-ей версии). Подход в Vuex 5 иной:

// store/greeter.js
import { defineStore } from 'vuex'

export default defineStore({
  name: 'greeter',
  state () {
    return { greeting: 'Hello' }
  }
})

// store/counter.js
import { defineStore } from 'vuex'
import greeterStore from './greeter'

export default defineStore({
  name: 'counter',

  use () {
    return { greeter: greeterStore }
  },
  
  state () {
    return { count: 0 }
  },
  
  getters: {
    greetingCount () {
      return `${this.greeter.greeting} ${this.count}'
    }
  }
})


Мы импортируем стор, затем регистрируем его через use и тем самым получаем к нему доступ. Все выглядит еще проще если использовать Сomposition API:

// store/counter.js
import { ref, computed } from 'vue'
import { defineStore } from 'vuex'
import greeterStore from './greeter'

export default defineStore('counter', ({use}) => {
  const greeter = use(greeterStore)
  const count = 0

  const greetingCount = computed(() => {
    return  `${greeter.greeting} ${this.count}`
  })

  return { count, greetingCount }
})


Единственное, о чем стоит упомянуть, это use, который работает точно также как vuex.store и отвечает за правильную инициализацию сторов.

Поддержка TypeScript


Благодаря изменениям в API и уменьшению количества абстракций, поддержка TypeScript в 4 версии будет намного лучше, но нам все еще потребуется много ручной работы. Выход же 5 версии даст возможность добавлять типы, там где необходимо, и там где мы этого хотим.

Заключение


Vuex 5 выглядит многообещающе и именно так как многие и ожидают (устранение старых недочетов, добавление гибкости). Полный список обсуждений и мнения основной команды можно найти в Vue RFCs репозитории.

© Habrahabr.ru