Over the years, frontend applications have become increasingly complex and stratified. As more and more data is being handled in the frontend, perhaps the most important challenge is managing the various complex states a client application can be in. This is why state management is perhaps the most critical challenge in building robust frontend applications.
In the past few years, Redux, MobX, and Vuex have emerged as the most popular libraries for state management in the JavaScript ecosystem. Despite Redux probably being the most popular of them, Vuex has cemented its place as the de facto state management library for web applications built on Vue.js, primarily due to its relative simplicity and its integration with Vue.
Both Redux and Vuex are inspired by the Flux architecture. However, the major difference is in how they handle state. In Redux, which is more popular with React developers, state is absolutely immutable; with Vuex, you have specific mutation rules that are much more approachable, less verbose, and more intuitive.
Vue components get their state from the Vuex store, which is essentially an object, but with distinct characteristics from a plain JavaScript object: they are reactive, and you cannot directly mutate the store’s state. The only way to change a store’s state is by explicitly committing mutations.
Vuex has gone through quite a number of iterations to get to its current state. In this article, we’d focus on what’s new in the latest version of VueX, version v4.0.0-beta
.
The major goals of version 4 are to support the new Composition API introduced in Vue 3 and to simplify the usage of Vuex overall. It is also intended to support a more robust inference for TypeScript. We will explore each of these in detail with few examples.
As of the time of writing, [email protected]
had just been released. One of the major breaking changes is the removal of the global typings for this.$store
within Vue components. The aim of this feature is to enable typescript users to compose full typing layers in components. Hence, developers can do manual declarations that will enable fully typed structures that were nearly impossible in Vuex 3.
In summary, according to the release notes:
When using TypeScript, you must provide your own augment declaration.
Here’s the example provided in the release notes above:
// vuex-shim.d.ts declare module "@vue/runtime-core" { // Declare your own store states. interface State { count: number } interface ComponentCustomProperties { $store: Store<State>; } }
We will explore this in deeper detail next.
The definition of a store typically starts with defining the state
:
interface Actor { name: string age: number } interface State { loading: boolean data: Array<Actor> } export const state:State = { loading: false, data: [{name: 'John',age: 25}]
It is important to export the type of state because it will be used in the definitions of getters, as well as mutations and actions. There is an accompanying GitHub repo for the examples in this post here.
Just like in Redux and other Flux implementations, it is common to store mutations as constants. The approach here would be to store all our potential mutations as a MutationTypes
enum.
// mutation-types.ts: export enum MutationTypes { SET_LOADING = 'SET_LOADING', FETCH_ACTORS = 'FETCH_ACTORS', ADD_ACTOR = 'ADD_ACTOR', REMOVE_ACTORS = 'REMOVE_ACTOR' }
We can now declare a “contract” for each of our mutations. This is a common pattern in Redux, analogous to reducers
. A mutation is simply a function that accepts the state and an optional payload, mutates the state, and returns the newly updated state.
With our newfound knowledge, let’s declare a type for our mutation. We have a couple of options for doing this; the more robust way is to employ TypeScript’s generic
for typing our mutation:
// mutuations.ts import { MutationTypes } from './mutation-types' import { State } from './state' export type Mutations<S = State> = { \[MutationTypes.SET_LOADING\](state: S, payload: boolean): void }
Looking good so far? Now let’s write an implementation:
import { MutationTree } from 'vuex' import { MutationTypes } from './mutation-types' import { State,Actor } from './state' export type Mutations<S = State> = { \[MutationTypes.SET_LOADING\](state: S, payload: boolean): void, \[MutationTypes.REMOVE_ACTORS\](state: S, payload: Actor): Array<Actor>, \[MutationTypes.ADD_ACTOR\](state: S, payload: Actor): Array<Actor>, } export const mutations: MutationTree<State> & Mutations = { \[MutationTypes.SET_LOADING\](state, payload: boolean) { state.loading = payload return state }, \[MutationTypes.REMOVE_ACTORS\](state, payload: Actor) { state.data = [payload] return state.data }, \[MutationTypes.ADD_ACTOR\](state, payload: Actor) { state.data = [payload] return state.data } }
The mutations
store all potential implemented mutations. This will be eventually used to construct the store.
Let’s write a simple action
for our application. First, let’s type the action
type with an enum:
// action-types.ts export enum ActionTypes { GET_ACTOR = 'GET_ACTORS', } ``` `actions.ts` ``` import { ActionTypes } from './action-types' export const actions: ActionTree<State, State> & Actions = { async \[ActionTypes.GET_ACTORS\]({ commit }) { const allActors = await fetch('actorsAPI').then((actors)=> commit(MutationTypes.ADD_ACTOR,actors)) return allActors }, }
Getters can also be statically typed. A getter isn’t much different from a mutation, as it is essentially a function that receives a state and performs a computation on it.
The below showcases an example that takes the state
as the first argument and returns the actor
name in uppercase. This can get really complex, but it follows the same basic principles:
// getters.ts import { GetterTree } from 'vuex' import { State } from './state' export type Getters = { capitalizeName(state: State): string[] } export const getters: GetterTree<State, State> & Getters = { capitalizeName: (state) => { return state.data.map(actor => actor.name.toUpperCase()) }, }
$store
typeAs mentioned earlier, you now have to explicitly type your store to access them in your components. So all default Vuex types — getters, commit, and dispatch — will be replaced by our custom types.
To make our defined types globally accessible and correctly working, we need to pass them to Vue, like so:
// vuex-shim.d.ts import {State} from '../state' declare module "@vue/runtime-core" { // Declare your own store states. interface State { count: number } interface ComponentCustomProperties { $store: Store<State>; } }
Now that we have a typed store, let’s utilize it in a component to solidify the concepts. We will be looking at usage in a component with the Composition API syntax since this is a major for change for Vue and one of the core purpose in Vuex 4.
We must access the store through the useStore
hook with the Composition API. The useStore
hook essentially returns our store
:
export function useStore() { return store as Store } <script lang="ts"> import { defineComponent, computed, h } from 'vue' import { useStore } from '../store' import { MutationTypes } from '../store/mutation-types' import { ActionTypes } from '../store/action-types' export default defineComponent({ name: 'CompositionAPIComponent', setup(props, context) { const store = useStore() const actors = computed(() => store.state.data) const capitalizeActors = computed(() => store.getters.capitalizeName) async function removeActor() { store.commit(MutationTypes.REMOVE_ACTORS, {name: 'John',age: 67}) } async function addActor() { const result = await store.dispatch(ActionTypes.GET_ACTORS, {name: 'John',age: 67}) return result } return () => h('section', undefined, [ h('h2', undefined, 'Composition API Component'), h('p', undefined, actors.value.toString()), h('button', { type: 'button', onClick: addActor }, 'Add Actor'), h('button', { type: 'button', onClick: removeActor}, 'Remove Actor'), ]) }, }) </script>
What we get is a fully statically typed store. We can only commit/dispatch declared mutations/actions with appropriate payloads; otherwise, we get an error.
This is fantastic for static analysis, which is great for self-documenting code. If you tried to send a malformed payload or an uninitiated action you have the TypeScript compiler yelling at you and guiding you to correctness. That’s awesome, isn’t it?
We have looked at what’s new and what’s in the pipeline for Vuex 4. At the time of writing, v4.0.0-beta.1
is the only version of Vuex 4 with major feature releases with a breaking change — that is, robust typing for the store and excellent integration with the Composition API in Vue 3.
v4.0.0-beta.3
also comes with a major feature: the createLogger
function was exported from Vuex/dist/logger
, but it’s now included in the core package. You should import the function directly from the Vuex package. This is a small but important feature to take note of.
It is imperative to remember that Vuex 4 is still in beta and is thus tagged as pre-release, implying that we can expect more exciting changes that have not been fully implemented or released. These include getting rid of mapXXX
and eliminating the need to separate actions and mutations.
It is such an exciting time to be a fan and a user of Vue and Vuex . Be on the lookout for the official release of Vuex 4 with all the amazing new features — I am particularly delighted with enhanced static typing. Once again here is an accompanying GitHub repo for the code examples.
Debugging Vue.js applications can be difficult, especially when there are dozens, if not hundreds of mutations during a user session. If you’re interested in monitoring and tracking Vue mutations for all of your users in production, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens in your Vue apps, including network requests, JavaScript errors, performance problems, and much more. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred.
The LogRocket Vuex plugin logs Vuex mutations to the LogRocket console, giving you context around what led to an error and what state the application was in when an issue occurred.
Modernize how you debug your Vue apps — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]
4 Replies to "What’s new in Vuex 4"
Your action ActionTypes.GET_ACTORS does not require payload. Why you give payload, when calling it ? 🙂
What would be the changes needed for namespaced modules?
I’ve been wondering maybe reactive store is more Typescript oriented, like this:
https://github.com/shemeshg/PWorkbook
(regarding Vuex) “The only way to change a store’s state is by explicitly committing mutations.”
That is not quite correct. Vuex store is mutable. If one change a property of an object loaded from Vuex store to some component the property would be also changed in the store.