Do you need Vuex? Learn Vue 3 approach to state management

While working with Vue 2, you may find yourself in a situation where the state or methods you are creating are needed in a few different places across the app. To start with, you don’t want to repeat yourself (DRY for life). And of course you don't want your props drilling through other components (parent and child components). Sometimes it is even impossible, and for sure it’s extra hard to maintain. There comes Vuex. What is Vuex? It is a state management pattern and library to keep your app maintainable, clean and easy to debug. The first thing you need to understand is that in Vue 3 you can still use Vuex, with a few tweaks, but honestly, you don’t need to. Truth has been spoken. From my own experience, Vuex is no longer needed, even if you are building enterprise-level web applications.

In this post, we will take a look at:

  • How to create composable to replace Vuex store;
  • How to keep global state non-changeable by every composable instance;
  • How to reuse that composable in different places of your application;
  • Caveats of using composable as a state management;
  • Pinia as an alternative to composables and Vuex.

We will also deal with the most tricky question: Do I really need Vuex for my next Vue project?

Make sure to read “How to start with Vue 3 Composition API and why?” to see how to handle reusable logic in Composition API with composables, which are scalable methods for your application. With that knowledge, you can entirely replace the Vuex store, and use only composables with shared logic. That is one of the challenges developers should be prepared to overcome. How to do that? Let’s see.

Vuex state management in Vue 2 

Despite the fact that Vue 3 is now the default version, there are still many developers who prefer to stay on Vue.js 2. Because of this, let's start with the basic Vue 2 project. Your file structure should look like this:

In this example, we will try to display the value from Counter which is inside TheHeader. Why? Why not!? That’s quite a real-life example, because there are plenty of situations where you have to display the same value in a few different components, and these components are totally not related.

In this case, components will have the following structure:

<template>
 <div id="app">
   <TheHeader  />
   <BaseWrapper />
 </div>
</template>
 
<script>
import TheHeader from './components/TheHeader.vue';
import BaseWrapper from './components/BaseWrapper.vue';
 
export default {
 name: 'App',
 components: {
   TheHeader,
   BaseWrapper,
 },
};
</script>

As you can see, TheHeader is inside App, but Counter is placed deeper, inside BaseContainer which is inside BaseWrapper.

<template>
 <div class="counter">
   <div>{{ FG }}</div>
   <div>
     <button @click="increment">+</button>
     <button @click="decrement">-</button>
   </div>
 </div>
</template>
 
<script>
export default {
 name: 'Counter',
 data() {
   return {
     initialCount: 0,
   };
 },
 methods: {
   increment() {
     this.initialCount++;
   },
   decrement() {
     this.initialCount--;
   },
 },
};
</script>

That makes it really hard to move that state upper and show it in TheHeader. To do that, you should probably go with event emitting, or even with an event bus. But trust me – you don’t want to do this.

Whole app looks like this:

With Vuex, it’s quite easy to transfer that counter state to global state, and show it in Counter and TheHeader components. You just have to move the initialCount, as well as the increment and decrement methods from the Counter component to the state store. I’m assuming that you are already familiar with Vuex, if not, dig first into the official guide.

Here’s main.js:

import { createApp } from 'vue'
import App from './App.vue'
import { store } from './store/index'
 
const app = createApp(App)
app.use(store)
app.mount('#app')

And here is your simple store, inside store/index.js:

import { createStore } from 'vuex'
 
export const store = createStore({
 state () {
   return {
     initialCount: 0
   }
 },
 mutations: {
   increment(state) {
     state.initialCount++;
   },
   decrement(state) {
     state.initialCount--;
   },
 }
})

So, in both Counter.vue and TheHeader.vue you can use mapState to get the value and print it in right spot:

 computed: {
   ...mapState({
     initialCount: state => state.initialCount
   })
 },

Now, these 2 Vue components can see and utilize counter value. However, Vuex is not the only or even best option for managing state. You will find out the better alternative further in this article.

You're now familiar with the basics. Vue Composition API and composables can replace Vuex, let’s see how it’s done.

Use Vue 3 Composition API instead of Vuex

As you already know from the previous article, composables are used for common logic, which is going to be shareable across your web app. Sounds familiar, right? Also, it replaces old and hard to maintain mixins. With Vue composables, you can create modules that are going to act like single Vuex store instances. You can have as many modules as you want, and use them across your application to share state or methods. 

Take a look again at the last example with a counter, but this time, let’s get rid of the Vuex store and create a completely new composable useCounter.

That's your project structure now:

useCounter will have both of our incrementing and decrementing methods inside, so let’s create an initial state with the increment and decrement methods:

import { ref } from "vue";
 
const useCounter = () => {
 const initialCount = ref(0);
 
 const increment = () => {
   initialCount.value++;
 };
 const decrement = () => {
   initialCount.value--;
 };
};
 
export default useCounter;

So you have your initialCount, which is ref with value "0", and two methods to increment and decrement that value. After that you need to return from composable everything that you are going to use outside:

import { ref } from "vue";
 
const useCounter = () => {
 const initialCount = ref(0);
 
 const increment = () => {
   initialCount.value++;
 };
 const decrement = () => {
   initialCount.value--;
 };
 
 return {
   initialCount,
   increment,
   decrement,
 };
};
 
export default useCounter;

With that, you are ready to use initialCount in TheHeader and Counter.

In Counter.vue:

<script setup>
import useCounter from '../composables/useCounter'
 
const {
 increment,
 decrement,
 initialCount
} = useCounter()
</script>

In TheHeader.vue:

<script setup>
import useCounter from '../composables/useCounter'
 
const {
 initialCount
} = useCounter()
</script>

But there’s something odd, look at the app:

Value in TheHeader is not updating, but it’s using the same value as in Counter.vue. That’s because we have created a composable that keeps our shareable logic and can be used in different components. Everytime when the Vue 3 component renders, the useCounter() method is called again, so basically, we have two instances of useCounter(): one in Counter.vue and the other one in TheHeader.vue. It is ok in case you are not building the state management composable. So how to fix that? You just need to move your state out of the composable declaration, and with that, you are using a singleton, which keeps only one instance of reactive object throughout the entire lifecycle, and ensures the state is fresh every time the user revisits the page:

import { ref } from "vue";
 
const initialCount = ref(0);
 
const useCounter = () => {
 const increment = () => {
   initialCount.value++;
 };
 const decrement = () => {
   initialCount.value--;
 };
 
 return {
   initialCount,
   increment,
   decrement,
 };
};
 
export default useCounter;

Now, TheHeader.vue and Counter.vue are using the same instance of state, and each of those components can show the same data:

Whenever you call useCounter(), the initialCount will stay the same across the whole app. So if you need to keep for example user data or other global state, you can keep it in composable, at the top level of your file, and that’s it!

Do I really need Vuex?

Long story short, no.

Basically, after working on a complex Vue application, with a lot of data which also has to be kept globally, I can easily say that you don’t need to use Vuex at all. Composables with global state are more than enough to keep everything working fine, and use reusable logic across the whole project. For me it’s a game changer, because making the composable state as a global one feels more predictable and easy to maintain. Every composable can act as a Vuex module, so it’s easy to find and keep them clean and updated. Additionally, you don’t need to install any additional packages, or configure anything. Composables are working out of the box, and have fully TypeScript support.

Is it safe for your data to use composable as a state management module?

Is it 100% safe and cool to use composable as a reusable state? Not always. It's perfectly fine when creating a single-page application, even if it’s going to be an enterprise-level application with tons of features. If it keeps SPA, you are fine. The problem comes when working with SSR (Server-Side Rendering), which can cause several vulnerabilities. Those singletons work great and keep fresh data whenever the user revisits the page, because initialisation of those files is fully on client-side. But in SSR, those modules are kept on server-side, and are initialized only once. So according to Vue.js docs, the cross-request state pollution can happen, when other users are receiving data (not necessarily their data) during a particular request. You can read about possible solutions here.

Pinia vs Vuex

Other solutions? Try Pinia state management. It’s lightweight, easy and great when it comes to performance. Pinia is a store library dedicated to Vue 3, based on composables. It’s quite similar to Vuex, but as the documentation says – compared to Vuex, Pinia provides a simpler API with less ceremony, and offers Composition-API-style APIs. More important is that it has solid type inference support when used with TypeScript. Also it keeps all the SSR problems not applicable.

My reference? When building SPA – go on with state management inside Vue 3 composable. When implementing SRR – go for Pinia because it’s more Vue 3-friendly.

Summary

Replacing Vuex store with composables is easy to do and later it's easily maintainable. Each composable can work as a module, and you can choose which data is going to act as a global state, and which is going to be a normal one. You can even combine a reusable global state with a local state. You can decide what suits your Vue app best. 

You can still use Vuex, but there’s no need for that. So you can get rid of unnecessary packages which are not required anymore. Especially if you are working with TypeScript, and you want the best typing possible.

If you are working on a SSR application, try out Pinia for fully TypeScript support, safeness and clear structure.

What is the best approach in your opinion? Is there something important we have missed? Would you like to extend our guide to Vue state management? Let us know by leaving a message on Twitter or LinkedIn.

P.S. If you find this article helpful, please remember to share it!


📣 If you would like to know more about Vue.js check out our series of the Vue 3 articles: