Pinia.js over Vuex for Vue.js state management

Navigating the vast landscape of state management in Vue.js applications, developers often face the critical decision of choosing between different solutions. In this article, we delve into the comparison of two prominent contenders: Pinia and Vuex. As the demand for efficient state management grows, understanding the nuances and capabilities of these libraries becomes paramount. Join us on a journey to dissect the features, performance, and developer experience offered by Pinia and Vuex

Difficulty Level: Advanced

Prerequisite: You should be familiar with state management in frontend frameworks.

Takeaway: This article gives you an overview of how pinia.js is different from Vuex state management.

Why state management?

Using Vue, the web page is designed by splitting into vue components, each component is an independent and reusable piece in the web page. The state of the component is not shared with other components or the nearest parent or its children, it's a closed container with HTML, CSS and javascript.

Vue has the ability for a component to communicate with the nearest/ immediate parent and its nearest children in the components hierarchy. Vue uses props to communicate from parent to children and event emitters to communicate from child to parent component.

Vue components communication diagram

Props are custom attributes we can register on a child component. When a value is passed to a prop attribute from parent component, it becomes a property on that child component instance.

Then the child component can emit an event on itself by calling the built-in $emit method, passing the name of the event

Parent component

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<template>
  <main>
    <WelcomeToVue :title="greetingMessage" @title-updated="updateGreetingMessage" />
  </main>
</template>

<script>
  import WelcomeToVue from '@/components/WelcomeToVue.vue'

  export default {
    name: 'HomeView',
    components: {
      WelcomeToVue
    },
    data () {
      return {
        greetingMessage: ''
      }
    },
    methods: {
      updateGreetingMessage (title) {
        this.greetingMessage = title
      }
    }
  }
</script>

Child Component

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
  <div>
    {{ title }}
    <button @click="userClicked">Click Here</button>
    }
  </div>
</template>

<script>
export default {
  name: 'WelcomeToVue',
  props: {
    title: {
      type: String,
      default: ''
    }
  },
  methods: {
    userClicked () {
      this.$emit('title-updated', 'message updated')
    }
  }
}
</script>

The data communication between parent and nearest child and vice versa using props and event emitters fits for small web pages with less number of hierarchies of nested components, when the applications grow bigger, there should be many deeply nested components. To communicate with the last children of the hierarchy with the first parent it becomes complex to pass props and emit events on a complete chain of components. To achieve this “state management” comes into the picture.

Vue Store diagram

The state management is a centralized store for all components in the application. The component at any position on the web page can access the store directly to read and write data. Once the state is updated in store, it updates the state in all the components wherever it is used.

Vue ecosystem has Vuex and Pinia.js libraries for state management

Vuex and Pinia Logo

What is Vuex?

Vuex is the store of Vue application. A "store" is basically a container that holds the application state. There are two things that make a Vuex store different from a plain global object:

  1. 1. Vuex stores are reactive. When Vue components retrieve state from it, they will reactively and efficiently update if the store's state changes.
  2. 2. The store's state cannot mutate directly. The only way to change a store's state is by explicitly committing mutations. This ensures every state change leaves a track-able record, and enables tooling that helps us better understand our applications.

What is Pinia.js?

Pinia is a store library for Vue. Pinia is now the official state management library of Vue developed and maintained by Vue core team members.

How is Pinia different from Vuex?

Pinia has a Simpler API than Vuex, Pinia is modular by design. Vuex provides just one store which can have multiple modules within it. Whereas in Pinia, we can create multiple stores that can be imported into components directly where they’re needed.

Both Pinia and Vuex come with Devtools, and both support Vue 2.x and Vue 3.x, listed down some of the differences with code examples below.

Update State without mutation:

In Vuex the state is updated using mutations, but in Pinia, we can directly update the state in actions

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Vuex store index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    user: {}
  },
  mutations: {
    updateUser(state, payload) {
      state.user = payload
    }
  },
  actions: {
    setUser({ state, commit, dispatch }, payload) {
      payload = dispatch('formatPayload', payload)
      commit('updateUser', payload)
    },
    formatPayload(context, payload) {
      return payload
    }
  }
})

// Pinia store index.js
import { defineStore } from 'pinia'

export const userStore = defineStore({
  id: 'main',
  state: {
    user: {}
  },
  actions: {
    setUser(payload) {
      this.user = payload
    }
  }
})

Writeable state in Pinia:

In Vuex the only way to mutate the state is by committing mutations, which are synchronous transactions, But in Pinia, it has API mapWritableState to directly update the store state in any component

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// Vuex store
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    user: {}
  },
  mutations: {
    updateUser(state, payload) {
      state.user = payload
    }
  },
  actions: {
    setUser({ state, commit, dispatch }, payload) {
      payload = dispatch('formatPayload', payload)
      commit('updateUser', payload)
    },
    formatPayload(context, payload) {
      return payload
    }
  }
})

// component.vue
import { mapActions } from 'vuex'

methods: {
  ...mapActions(['setUser']),
  saveUser(payload) {
    this.setUser(payload)

    // or equivalent
    this.$store.dispatch('setUser')
  }
}

// Pinia
// directly we can update the store state
// component.js
import { mapWritableState } from 'pinia'
import { userStore } from '@/stores'

computed: {
  ...mapWritableState(userStore, ['user'])
},
methods: {
  setUser(payload) {
    this.user = payload
  }
}

No need of modules for namespacing:

In Vuex the store is divided into modules. Each module contains state, getters, mutations, actions and even nested modules

1
2
3
4
5
6
7
8
9
// Customer module
// /store/modules/customer.js
export default {
  namespaced: true,
  state: {},
  getters: {},
  mutations: {},
  actions: {}
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// store index.js
import Customer from '@/store/modules/customer'
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

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

In Pinia there are no namespaced modules, it offers flat structuring by design. To access other store instances, we can just import and access into the current store. Here is the example code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// index.js
import { defineStore } from 'pinia'

export const userStore = defineStore({
  id: 'main',
  state: {
    user: {}
  },
  getters: {
    userId() {
      return this.user.id
    }
  },
  actions: {}
})

// customer.js
import { defineStore } from 'pinia'
import { userStore } from '@/stores'

export const customerStore = defineStore({
  id: 'customer',
  state: {},
  getters: {}
  actions: {
    setCustomer (payload) {
      const userId = userStore().userId
      return { ...payload, userId }
    }
  }
})

Access to store instance in store:

In Vuex, while working on a store, one action or getter should have dependency or access to other actions' response or current store state. We can use context or first argument to access the current store properties. Here is the example code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    user: {}
  },
  getters: {
    activeCustomers(state) {
      // by default first argument is the state of current store object

      //logic goes here
    }
  },
  mutations: {
    updateUser(state, payload) {
      // by default first argument is the state of current store object
      state.user = payload
    }
  },
  actions: {
    setUser({ state, commit, dispatch }, payload) {
      // default first argument is the context of store instance, context is a object, it has access to state, mutations and actions
      // access mutation using commit
      // access actions using dispatch
      // access rootState if the store is a namespaced module
      // access rootActions if the store is a namespaced module
      // access rootGetters if the store is a namespaced module
      payload = dispatch('formatPayload', payload)
      commit('updateUser', payload)
    },
    formatPayload(context, payload) {
      return payload
    }
  }
})

In Pinia we can access the store instance directly from this object and there is no default first argument as store context

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { defineStore } from 'pinia'

export const userStore = defineStore({
  id: 'main',
  state: {
    user: {}
  },
  getters: {
    activeCustomers() {
      return this.customers.filter(item => item.isActive)
    }
  },
  actions: {
    setUser(payload) {
      payload = this.formatPayload(payload)
      this.user = payload
    },
    formatPayload(payload) {
      return payload
    }
  }
})

Import store into components:

In Vuex importing state, actions, getters into components uses Vuex API or directly access using store instance this.$store

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<template>
  <div>
    Home View
  </div>
</template>

<script>
import { mapState, mapGetters, mapActions } from 'vuex'

export default {
  name: 'HomeView',
  computed: {
    // import root state properties into component using array format
    ...mapState([
      'user',
      'project',
      'activities'
    ]),

    // import root state properities into component using Object format
    ...mapState({
      customerId: state => state.customerModule.customer.id,,
      userId: state => state.user.id
    }),

    // import namespaced module state property
    ...mapState('customerModule', ['customer']),

    // import root getters into component
    ...mapGetters(['activeUsers']),

    // import namespaced getters into component
    ...mapGetters('customerModule', ['activeCustomers'])
  },
  methods: {
    // import root actions into component
    ...mapActions(['setUser']),

    // import namespaced action into component
    ...mapActions('customerModule', ['setCustomer']),
    print() {
      // without using mapState, directly we can access root state and namespaced state in any component using store instance this.$store
      this.$store.state.user.id
      this.$store.state.customerModule.customer

      // without using mapActions, directly we can execute root actions and namespaced actions in any component using store instance this.$store
      this.$store.dispatch('setUser', payload, { root: true })
      this.$store.dispatch('customerModule/setCustomer', payload)
    }
  }
}
</script>

In Pinia importing state, actions and getters uses Pinia API and instance of store object

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<script>
import { mapState, mapActions, mapGetters } from 'pinia'
import { userStore } from '@/stores'
import { customerStore } from '@/stores/customer'

export default {
  name: 'HomeView',
  computed: {
    // import state and getters directly from store instance into component
    ...mapState(userStore, ['user']),
    ...mapGetters(userStore, ['userId']),
    ...mapState(customerStore, ['customer']),
    ...mapGetters(customerStore, ['activeCustomers'])
  },
  methods: {
    // import actions into component
    ...mapActions(userStore, ['setStore']),
    ...mapActions(customerStore, ['setCustomer'])
  }
}
</script>

Support for Typescript:

Vuex doesn't provide typings for this.$store property out of the box. When used with TypeScript, you must declare your own module augmentation.

In Pinia everything is typed and the API is designed in a way to leverage TS type inference as much as possible.

You need a help building a web application? Drop us a line here.

Written by:

 avatar

Chandra Sekar

Senior Frontend Developer

Chandra is an exceptional Vue.js frontend developer who consistently delivers outstanding results. With a keen eye for detail and a deep understanding of Vue.js, Chandra transforms design concepts into seamless and visually captivating user interfaces.

Read more like this: