Vuex 5: A Closer Look at the Planned Overhaul

Vuex has long been the standard for managing state in Vue applications. While Vuex 4 is nearing release with full Vue 3 compatibility, it doesn't introduce any new features. However, Vue core team member Kia King Ishii has been discussing plans for Vuex 5, and the proposed changes represent a significant shift in how developers will interact with the library. These plans are still in flux, but the direction is clear: Vuex 5 aims to combine the flexibility of hand-rolled composition API stores with the robust ecosystem of an official library — including devtools, documentation, and Nuxt integrations.

The motivation for this overhaul is partly driven by the rise of composition API alternatives. These patterns work well for small apps but lack the tooling and community support that come with an official solution, particularly the Vue devtools inspector with its time-travel debugging capabilities. Vuex 5 is designed to offer the best of both worlds.

Defining a Store

The current Vuex 4 model requires a store to contain four distinct parts: state, getters, mutations, and actions, each with its own rules and syntax. Vuex 5 dramatically simplifies this structure:

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')
    }
  }
})
import { defineStore } from 'vuex'

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

Notable changes include replacing createStore with defineStore and the addition of a required name property. The concept of modules is eliminated; each module becomes its own independent store with its own name. State must now be a function that returns the initial state, similar to the data option in components. Getters and actions can access store properties directly via this instead of receiving parameters or a context object. The mutations section is gone entirely, with actions absorbing that responsibility. The rationale is that mutations often devolve into simple setters, adding verbosity without much benefit.

For developers who prefer the composition API for defining components, a similar approach is available for creating stores:

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 }  
})

This style passes the store name as the first argument, and the rest resembles a typical composition function, yielding the same result as the options API example.

Store Instantiation and Access

In Vuex 4, calling createStore immediately instantiates the store, and it can be used within the app via app.use or directly.

import { createApp } from 'vue'
import App from './App.vue' // Your root component
import store from './store' // The store definition from earlier

const app = createApp(App)

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

// Now all your components can access it via `this.$store`
// Or you can use in composition components with `useStore()`

// -----------------------------------------------

// Or use directly... this is generally discouraged
import store from './store'

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

Vuex 5 introduces a more explicit setup. Each application can get its own Vuex instance via createVuex(). This ensures separate apps can have separate instances of the same store without shared data interference, though you can share a single Vuex instance if you want to share store instances across apps.

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

const app = createApp(App)
const vuex = createVuex() // create instance of Vuex

app.use(vuex) // use the instance
app.mount('#app')

Once the Vuex instance is installed, you don't pass root store definitions upfront. Instead, stores are imported directly into the components that use them and registered with that particular Vuex instance:

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

export default defineComponent({
  name: 'App',

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

Calling $vuex.store instantiates and registers the store if it hasn't been already, and returns the same instance thereafter. The same method is accessible directly on the Vuex object from createVuex(). The store is then accessible on the component as this.counter. For composition API components, a useStore helper serves the same purpose.

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

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

    return { counter }
  }
})

Importing stores directly into components enables code splitting and lazy loading, but it makes the store a direct dependency. If you prefer a more classic dependency injection pattern, you can use provide at an appropriate level, such as the app root:

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('store', store) // provide the store to all components
app.mount('#app')

Any child component can then use inject to gain access to the store:

import { defineComponent } from 'vue'

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

// Or with Composition API

import { defineComponent, inject } from 'vue'

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

    return { store }
  }
})

This adds some upfront verbosity, but it creates a more explicit and flexible architecture. This kind of setup is typically done once at the start of a project, and it aligns Vuex usage with how developers usually handle other dependencies.

Using a Store

In Vuex 4, accessing state, getters, mutations, and actions requires different syntax for each:

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

While explicit, this disjointed API becomes cumbersome, especially with namespaced modules. Vuex 5 unifies this, placing everything — state, getters, and actions — directly at the root of the store:

store.count        // Access State
store.double       // Access Getters (transparent)
store.increment()  // Run actions
// No Mutators

This removes the need for helper methods like mapState, mapGetters, mapActions, and mapMutations, or writing extra computed statements for the composition API. A Vuex store will now look and behave much like a custom-built store, which is a major improvement in usability.

Composing Multiple Stores

The removal of namespaced modules means each logical piece of state is handled as its own independent store. Components can easily import and use one or multiple stores. Inter-store interaction is also straightforward. Instead of dealing with the convoluted namespace and rootGetters logic of the past, you simply import and register the other store:

// 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' // Import the store you want to interact with

export default defineStore({
  name: 'counter',

  // Then `use` the store
  use () {
    return { greeter: greeterStore }
  },
  
  state () {
    return { count: 0 }
  },
  
  getters: {
    greetingCount () {
      return `${this.greeter.greeting} ${this.count}' // access it from this.greeter
    }
  }
})

With the composition API style, this is even cleaner:

// store/counter.js
import { ref, computed } from 'vue'
import { defineStore } from 'vuex'
import greeterStore from './greeter' // Import the store you want to interact with

export default defineStore('counter', ({use}) => { // `use` is passed in to function
  const greeter = use(greeterStore) // use `use` and now you have full access
  const count = 0

  const greetingCount = computed(() => {
    return  `${greeter.greeting} ${this.count}` // access it like any other variable
  })

  return { count, greetingCount }
})

In both examples, the use method functions similarly to the vuex.store mechanism, ensuring the store is instantiated with the correct Vuex instance. This fundamentally simplifies how modules relate to each other, treating each as a self-contained unit that can optionally depend on others.

TypeScript Improvements

The simplification in Vuex 5 is also good news for TypeScript users. Previous versions of Vuex, with their multiple layers of abstraction and separation between state, getters, mutations, and actions, made achieving proper type safety nearly impossible without extensive manual work. The new, flatter architecture allows types to be defined inline in a natural way, drastically reducing the burden of adding TypeScript support.

What This Means

Vuex 5 looks set to address many of the usability and workflow complaints that have accumulated over the years. It simplifies the mental model by removing redundant concepts like mutations and modules, and it makes the API more consistent and approachable. The extra flexibility and explicitness in how stores are created and injected might add some verbosity to dependency management, but it also makes the library more adaptable to different application sizes and code styles. While these plans are far from final and could be altered, the proposed direction promises a notable upgrade to the everyday developer experience.