- Start Date: 2021-03-02
- Target Major Vue Version: 3.1.x
- Target Major Vuex Version: 5.0.0
- Reference Issues: N/A
- Implementation: N/A
Introducing a brand new Vuex for Vue 3. It's designed to improve various architecture and API of Vuex from what we've learned from past years. Here are the key differentiators from Vuex 3 & 4.
- 2 syntax support for the store creation, options api, and composition api.
- No mutations. Only state, getters, and actions.
- No nested modules. Only stores. Compose them instead.
- Complete TypeScript support.
- Transparent, automated code splitting.
import { createVuex, defineStore } from 'vuex'
const vuex = createVuex()
const useCounter = defineStore({
key: 'counter',
state: () => ({
count: 1
}),
getters: {
double() {
return this.count * 2
}
},
actions: {
increment() {
this.count++
}
}
})
const counter = vuex.store(useCounter)
counter.count // 1
counter.double // 2
counter.increment()
counter.count // 2
counter.double // 4
Vuex was introduced as an official Flux-like implementation of a centralized state management solution for Vue. While it serves the purpose of implementing a Flux architecture in Vue, it also has a great responsibility to provide an official way to share the state between Vue Components which are not in parent-child relationship.
Getting feedbacks until now, seeing the rise of Vue Composition API and many alternative state management solutions, this RFC focuses on making Vuex an official global state management tool for Vue, rather than it being a Flux library.
Having flux architecture is not a requirement for global state management, but it's just one of the best practices. For global state management, what we really need is;
- Define global states, and provide a way to reference and mutate them.
- Code splitting
- Support SSR.
- Support Vue Devtools.
- Extensible to implement any other state management solution on top of it.
We got lots of questions saying "do we still need Vuex for Vue 3? (in favor of Composition API)". But It would take a lots of effort to support all of these features with just plain composition functions. Vuex should provide all of the features required to consume the global state management in the Vue app and its ecosystem.
Hopefully, the ideal scenario could be users to first reach out to Vuex for global state management, and then build a more advanced global state management feature on top of Vuex so that they don't have to worry about SSR or Devtools support. It should benefit other frameworks such as Nuxt to be compatible with such advanced state management tool.
Vuex 5 introduces quite new ideas on how to define, create, and manage the store. In this section, we'll walk through each step of the new Vuex usage.
A store can be defined using defineStore
function. The function will return a composable function that can be used to create a store. At here, we're defining a "counter" store and naming the returned function as useCounter
.
import { defineStore } from 'vuex'
export const useCounter = defineStore({
key: 'counter',
state: () => ({
count: 1
}),
getters: {
double() {
return this.count * 2
}
},
actions: {
increment() {
this.count++
}
}
})
The differences from Vuex 3 & 4 are that:
- No "mutations". "Actions" can directly mutate the state.
- We may access other store properties through
this
context, just like in Vue Component. For example, if we want to accesscount
state inincrement
action, we can simply referencethis.count
property.
As the name defineStore
suggests, useCounter
is not the actual store instance. In order to interact with this store, we must first create a new Vuex instance, and create a new store instance through vuex.store
method.
We can create a new Vuex instance via createVuex
method.
import { createVuex } from 'vuex'
const vuex = createVuex()
In Vuex 5, the store is an individual component that acts very similar to "modules" in Vuex 3 & 4. The new Vuex instance will behave as a container for those stores.
After creating a Vuex instance, we may now create the "counter" store by passing the useCounter
function to the vuex.store
method.
const counter = vuex.store(useCounter)
The created store instance will have all properties (state, getters, and actions) directly mapped to the instance it self. Which means, you may call any defined properties like this.
// Reference state.
counter.count // <- 1
// Reference getters.
counter.double // <- 2
// Call actions.
counter.increment()
There's no getters
or dispatch
method to access store properties. We can access everything, just like how we would access data
or methods
in Vue Component.
Now we know how we can define and use a store, but how do we use it inside Vue Component? To use the store inside Vue Component, we must first register the Vuex instance to the Vue App instance through app.use
method.
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')
After installing Vuex, you can retrieve the store via the injected this.$vuex.store
method.
import { useCounter } from '@/stores/counter'
export default {
computed: {
counter () {
return this.$vuex.store(useCounter)
},
count() {
return this.counter.count
},
double() {
return this.counter.double
}
},
methods: {
increment() {
this.counter.increment()
}
}
}
We may retrieve the store by using the mapStores
helper function as well.
import { mapStores } from 'vuex'
import { useCounter } from '@/stores/counter'
export default {
computed: {
...mapStores([
useCounter
]),
count() {
return this.counter.count
},
double() {
return this.counter.double
}
},
methods: {
increment() {
this.counter.increment()
}
}
}
In Composition API, you may directly call the store definition without passing it to the vuex.store
method.
import { useCounter } from '@/stores/counter'
export default {
setup() {
const counter = useCounter()
return {
counter
}
}
}
In this section, we'll go through each API details.
Vuex 5 comes with 2 different syntax support for defining a store. The option syntax and composition syntax. From here on, we'll refer to them as "Option Store" and "Composition Store".
An option store can be defined as below.
import { defineStore } from 'vuex'
const useCounter = defineStore({
key: 'counter',
state: () => ({
count: 1
}),
getters: {
double() {
return this.count * 2
}
},
actions: {
increment() {
this.count++
}
}
})
key
- The unique identifier for the store. It's used to identify the store in Dev Tool and SSR hydration process. Because we must be able to serialize the key, it has to be astring
.state
- Same as state in Vuex 3 & 4. However in Vuex 5, it must be a function.getters
- Similar to getters in Vuex 3 & 4, but it will not receive any arguments. To reference the state, access it throughthis
context.actions
- Similar to actions in Vuex 3 & 4, but it can mutate the state directly. It may be an async function as well. actions will not take context, but you may define any arguments like any ordinal functions. You may access state and getters throughthis
context as same asgetters
.
Option Store aligns very well to Vue Component option syntax. state
, getters
, and actions
can easily move into Vue Component as data
, computed
, and methods
property. They behave almost identical as well.
A composition store can be defined as below.
import { ref, computed } from 'vue'
import { defineStore } from 'vuex'
const useCounter = defineStore('counter', () => {
const count = ref(1)
const double = computed(() => count.value * 2)
function increment() {
count.value++
}
return {
count,
double,
increment
}
})
The 1st argument passed to defineStore function is identical to key
property for Option Store. It serves as an identifier for the store. The 2nd argument is the setup
function. It behaves very similarly to Vue's setup
hook in composition api.
In fact, see how it uses Vue's native reactivity system, such as ref
and computed
to create reactive values. In Composition Store, you're free to use any other reactivity system, such as reactive
.
To make store usable, we must pass a Store Definition to the Vuex instance. The Vuex instance is responsible for registering stores, and handle the store composition (will discuss it later).
It's mandatory to have this centralized container of stores to avoid making a store to become global singleton, and support SSR. All stores should be managed by the Vuex instance, which is most likely injected into the Vue App instance.
To create a store, we may use vuex.store
method. The store
method will unwrap Ref
values and provide direct access to the store properties.
When you pass the Option Store to the store method, it won't make any noticeable difference.
import { createVuex } from 'vuex'
import { useOptionCounter } from '@/stores/optionCounter'
const vuex = createVuex()
const counter = vuex.store(useOptionCounter)
counter.count // <- 1
counter.double // <- 2
However, when you pass the Composition Store to the store
method, all of the ref
values will be unwrapped.
import { createVuex } from 'vuex'
import { useCompositionCounter } from '@/stores/CompositionCounter'
const vuex = createVuex()
const counter = vuex.store(useCompositionCounter)
counter.count // <- 1. No need for counter.count.value
counter.double // <- 2. No need for counter.double.value
Remember that counter.count
was defined as ref
, and counter.double
was defined as computed
, though when accessing them, we don't need to reference counter.count.value
nor counter.double.value
.
When creating stores through Vuex store
method for the 1st time, Vuex will first generate store, make state reactive, then registers them to the internal store registry. Then when creating the same store for the second time, it will just return the store that is already registered.
This is going to eliminate the need for registering stores manually, as we did in Vuex 3 & 4 for Modules. We don't need things like registerModule
method since the store registration is now completely transparent and automatic.
It also makes code-splitting easy and efficient since stores are not registered until it gets used. Bundlers such as webpack and rollup should be able to handle it automatically.
In order to use store in Vue Component, at first, we'll register Vuex instance to the Vue App instance via app.use
method.
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')
After registering Vuex, we may access stores in Vue Component.
When using stores in Vue Options API, we can define which stores to use with the mapStores
helper function.
import { mapStores } from 'vuex'
import { useCounter } from '@/stores/Counter'
export default {
computed: {
...mapStores([
useCounter
]),
count() {
return this.counter.count
}
}
}
Under the hood, the mapStores
will call vuex.store
method to create the stores. Because it uses vuex.store
method, ref
values in Composition Store will be unwrapped as well. There's no need to do this.counter.count.value
to access Composition Store properties.
mapStores
takes an array of stores, and it uses the key
property as a binding name. In the above example, useCounter
is mapped as this.counter
because the useCounter
has key
named counter
.
In case we want have custom binding name, we may pass an object instead of an array.
import { mapStores } from 'vuex'
import { useCounter } from '@/stores/Counter'
export default {
computed: {
...mapStores({
myCounter: useCounter
}),
count() {
return this.myCounter.count
}
}
}
When using stores in composition api, we may directly call store definition.
import { useCounter } from '@/stores/Counter'
export default {
setup() {
const counter = useCounter()
counter.count // <- 1
}
}
Under the hood, it calls the vuex.store
method by retrieving Vuex instance via provide/inject
method of Vue. Therefore, using a Composition Store also return any reactive values unwrapped.
We may use a store inside another store. Let's say we have another store named "greeter" defined as below.
export const useGreeter = defineStore({
key: 'greeter',
state: () => ({
greet: 'Hello'
})
})
To use this store inside the counter
store, we can do so by using a store composition.
We can composite the greeter store to the counter store by defining greeter store in the use
option.
import { useGreeter } from './greeter'
const useCounter = defineStore({
key: 'counter',
use: [
useGreeter
],
state: () => ({
count: 1
}),
getters: {
countWithGreet() {
return `${this.greeter.greet} ${this.count}`
}
}
})
It works very similarly to how we use stores in Vue Component. As same as mapStores
helper function, the use
property can be defined as an object as well.
import { useGreeter } from './greeter'
const useCounter = defineStore({
key: 'counter',
use: {
myGreeter: useGreeter
},
getters: {
countWithGreet() {
return `${this.myGreeter.greet} ${this.count}`
}
}
})
Similar to when using a store inside the setup
hook, we may directly call store definition.
import { useGreeter } from './greeter'
const useCounter = defineStore('counter', () => {
const greeter = useGreeter()
const count = ref(1)
const countWithGreet = computed(() => {
return `${greeter.greet} ${count.value}`
})
return {
count,
countWithGreet
}
})
We can composite circular referenced stores as well, but with a limitation. Let's say we have "storeA" and "storeB", and both try to use each other.
In Option Store, it works without any limitation.
const useStoreA = defineStore({
key: 'storeA',
use: [
useStoreB
],
state: () => ({
fooA: 1
}),
getters: {
foo() {
this.storeB.fooB // Works!
}
}
})
const useStoreB = defineStore({
key: 'storeB',
use: [
useStoreA
],
state: () => ({
fooB: 1
}),
getters: {
foo() {
this.storeA.fooA // Works!
}
}
})
In Composition Store, we may only access store property inside function calls, like computed
. Otherwise the store properties become undefined
.
const storeA = defineStore('storeA', ({ use }) => {
const storeB = useStoreB()
// ERROR! `fooB` is `undefined`.
storeB.fooB
const bar = computed(() => {
// Yes, it works!
storeB.fooB
})
// NOPE! It wouldn't work. We get `undefined`.
bar.value
})
This is a JavaScript limitation on how circular reference works. And in general, you should try to avoid circular reference as a best practice. If two stores need to access/operate the same piece of state, then you should "hoist" that common part into its own dedicated store.
Vuex 5 supports the plugin feature. For example, a user might want to inject an external dependency, such as axios instance to the store. Vuex 5 provides plugins option similar to Vuex 3 & 4, but more aligned with how Vue 3 plugin works.
import { createVuex } from 'vuex'
import axios from 'axios'
function axiosPlugin(vuex, options) {
vuex.storeProperties.$axios = axios
}
const vuex = createVuex({
plugins: [axiosPlugin]
})
See the axios is injected into the storeProperties
. Anythinfg registered to this property will be available in both Option Store and Composition Store. In the Option Store, it will be available through this
context. And in Composition Store, it is passed through "context" object wich is passed as an argument to the setup function.
const useCounter = defineStore({
name: 'counter',
actions: {
async fetch() {
await this.$axios.get('...')
}
}
})
const useCounter = defineStore('counter', ({ $axios }) => {
async function fetch() {
await $axios.get('...')
}
return { fetch }
})
Both Option Store and Composition Store are fully type-safe. In Option Syntax, getters
and actions
may require annotation as same as option syntax Vue Component.
const useCounter = defineStore({
key: 'counter',
state: () => ({
count: 1
}),
getters: {
// May require annotation as same as Vue Component.
double(): number {
return this.count * 2
}
},
actions: {
// May require annotation as same as Vue Component.
increment(): void {
this.count++
}
}
})
For the Composition Store, everything is correctly typed as same as Vue Composition API.
For the plugins, the plugin author must provide a typing for StoreCustomeProperties
interface. This interface is extended by both Options Store and Composition Store context.
import { AxiosInstance } from 'axios'
declare module 'vuex' {
interface StoreCustomProperties {
$axios: AxiosInstance
}
}
We may provide other advanced features that are available in Vuex 3 & 4, such as watch
, subscribe
, etc. However, since the proposal is quite large already, those should be discussed in another proposal, or directly at issues or PRs as a feature request.
A store can be serialized so that users can save the store state to external storage, such as a cookie or index DB, and hydrate the state afterward. The same strategy applies to SSR state hydration, where the serialized state will be sent to the client through web request payload.
As an exmaple, assume we have an SSR app where the following store is used:
// universal API layer
import { fetch } from './api'
export const usePost = defineStore('post', () => {
const post = ref(null)
async function fetchPost(id) {
post.value = await fetch(id)
}
return {
post,
fetchPost
}
})
And in the route component, it calls await postStore.fetchPost(route.params.id)
which fills the post store with data. Now the question is, how do we serialize the state and how do we hydrate the post with the already fetched data?
- During serlization we want to just ignore:
- computed refs (considered getters)
- functions (considered actions)
- During hydration:
- for non-computed refs, we want to hydrate by setting its
.value
instead of replacing it. - for reactive objects, we need to mutate it in place to avoid replacing its reference.
- for non-computed refs, we want to hydrate by setting its
To serialize store, we may use vuex.serialize
method. To make the example simpler, here, we use the counter store as an example.
const useCounter = defineStore('counter', () => {
const count = ref(1)
function increment() {
count.value++
}
return {
count,
increment
}
})
// create a store and increment the `count` value
const counter = vuex.store(useCounter)
counter.increment()
// serialize the store
const state = vuex.serialize()
/*
{
counter: {
count: 2 // <- count is 2, because it was incremented.
}
}
*/
Now, users may store serialized state
where ever they want.
When serializing Option Store, there'll be no problem, though in Composition Store, we must always remember to "expose all state which might be mutated". For example, it's not possible to hydrate "hidden" property like this.
const useCounter = defineStore('counter', () => {
const count = ref(0)
const double = computed(() => count.value * 2)
function increment() {
count.value++
}
// `count` is not exposed! We can't serialize such hidden value.
return {
double,
increment
}
})
In Option Store, there's no way to "hide" state, so this only applies to the Composition Store.
We may use vuex.setState
method to hydrate the store by given serialized state.
const state = {
counter: {
count: 2
}
}
vuex.setState(state)
const counter = vuex.store(useCounter)
counter.count // <- 2
When a state is set via setState
method, the store gets hydrated during the store creation time. Therefore, note that setState
must be called before creating any store. Otherwise, it wouldn't take any effect.
The core feature to serialize and hydrate reactive object will be provided through Vue core, since this is useful for general composable functions as well. The RFC for that is planned to be published.
Although, Vuex still needs to provide vuex.serialize
method and vuex.setState
method to serialize/hydrate "multiple stores" in one shot.
It's essential for Vuex to have good Devtools support, and it will. However, Devtools support is another large topic that should require deep discussion. We should create another proposal focusing only on Devtools support exclusively.
The brief idea for Devtools support are;
- State can be inspected in the dev tools.
- When the state changes, it's logged, and the log entry should provide a way for the user to easily trace back to the source code that triggered it.
- Users should be able to "time travel" the state changes.
We should also work together with Devtools development to see if we can comeup with better Vuex debugging experience.
- The name "Vuex" might be a little confusing since initially it was named after Flux implementation. However, Vuex nowadays are referred to as more of an official global state management tool for Vue, I don't think we would have huge impact.
- Huge migration cost if a user is moving from Vuex 3 or 4 to Vuex 5.
- We don't have helper functions such as
mapState
ormapActions
, where it might be useful in Vue Options Component. However, since Vuex 3 & 4 didn't have a way to retrieve a store as an object like Vuex 5 does, it might be not as useful as it is in Vuex 3 & 4. Therefore we're excluding it in the initial spec, but it's technically possible to add such helpers, and we can always do so afterwards if we have high demands from the community.
Vuex 5 is almost completely new software compared to Vuex 3 & 4. There wouldn't be an easy migration, though, with community effort, we might be able to create a plugin that offers a similar API to Vuex 3 & 4. It might be one idea to make migration a bit easier.
None.