DEV Community

loading...
Cover image for Shared State Management with Vue Composition Api

Shared State Management with Vue Composition Api

nonso profile image Nonso Chukwuogor ・5 min read

Vue is one of the most impactful and popular frontend frameworks from the last decade. Its ease of use won the hearts of many software enthusiasts ranging from beginners to experts alike.

But like many component based frameworks, data management becomes an issue as an app begins to scale. The need for shared state becomes obvious and discussions on the best solution are usually divisive and subjective.

Vue solves this with the use of a first party external package called Vuex. It's a state management library intended for use with Vue. It does this by abstracting the state and mutations (methods meant to change the state) into a store that's available for any component to use.

Let's create a simple Vuex store that has a list of items, methods to add and delete items, and a computed value to get the total number of items in the store.

// store.js
import Vue from 'vue';
import Vuex from 'vuex';

// register vuex as a plugin with vue in Vue 2
Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    items: []
  },
  mutations: {
    ADD_ITEM(state, item) {
      state.items.push(item)
    },
    REMOVE_ITEM(state, id) {
      state.items = state.items.filter(item => item.id !== id)
    }
  },
  getters: {
    totalLength: state => state.items.length
  }
});

We'll create components that interact with the store.

// ItemForm.vue

<template>
  <form @submit.prevent="handleSubmit">
    <input v-model="value" required placeholder="Item Name">
  </form>
</template>

<script>
  export default {
    data:() => ({value: ''}),
    methods: {
      handleSubmit(){
        this.$store.commit('ADD_ITEM', {
          id: Math.random().toString(),
          name: this.value
        });
        this.value = ''
      }
    }
  } 
</script>

The ItemForm component allows you to add new items to the store by committing the ADD_ITEM mutation and passing the new item.

// Items.vue

<template>
  <div>
    <ul>  
      <li v-for="item in $store.state.items" :key="item.id">
        <span>{{item.name}}</span>
        <button @click="$store.commit('REMOVE_ITEM', item.id)">delete</button>
      </li>
    </ul>
    <div>
      Total Items: {{$store.getters.totalLength}}
    </div>  
  </div>
</template>

The Items component shows the list of items in the store and provides buttons for deleting each item by committing the REMOVE_ITEM mutation. It also shows the total count of items in the store using the totalLength getter.

// App.vue

<template>
  <div>
    <items />
    <item-form/>
  </div>
</template>

The App component composes the Item and ItemForm components

Vue 3 brings a lot of new apis and features that make data and logic organization a lot cleaner and more reusable. Let's see how we can model the same behaviour using Composition API introduced in vue 3 as well as the existing provide/inject api

// items-provider.js

import { reactive, computed, readonly } from "vue";

const state = reactive({
  items: []
})

function addItem(todo) {
  state.items.push(todo);
}

function removeItem(id) {
  state.items = state.items.filter(item => item.id !== id);
}

const totalLength = computed(() => state.items.length);

export const itemStore = readonly({
  state,
  totalLength,
  addItem,
  removeItem
});

reactive as the name implies, creates a reactive object that notifies its dependencies whenever its properties change. e.g, if the reactive object's property is referenced in a vue component's template, the component is registered as a dependency of that object's property and re-renders whenever that property changes.

computed accepts a function and returns a memoized value that gets updated whenever any of the reactive values referenced in the callback function gets updated.

readonly creates a read-only object. If an attempt is made to mutate any property on the object, a warning message is shown in the console and the operation fails.

Since the items-provider.js file is a module, we only expose/export what we need (in this case, itemStore). External modules and components shouldn't have direct access to mutate the items and properties of the store, so we expose a readonly version of the store.

We can now rewrite our components like so

// App.vue

<template>
  <items />
  <item-form />
</template>

<script>
  import { itemStore } from './items-provider'
  export default {
    provide: {
      itemStore
    }
  }
</script>

In the App component we provide the itemStore to make it injectable in any descendant component.

Also note that in vue 3, you aren't limited to just one root element per component

In the child components, we inject the itemStore and it becomes available within the component's context.

// Items.vue

<template>
  <ul>  
    <li v-for="item in itemStore.state.items" :key="item.id">
      <span>{{item.name}}</span>
      <button @click="itemStore.removeItem(item.id)">delete</button>
    </li>
  </ul>
  <div>
    Total Items: {{itemStore.totalLength}}
  </div>    
</template>

<script>
  export default {
    inject: ['itemStore']
  }
</script>

// ItemForm.vue

<template>
  <form @submit.prevent="handleSubmit">
    <input v-model="value" required placeholder="Item Name">
  </form>
</template>

<script>
  export default {
    inject: ['itemStore'],
    data: () => ({value: ''}),
    methods: {
      handleSubmit(){
        this.itemStore.addItem({
          id: Math.random().toString(),
          name: this.value
        });
        this.value = ''
      }
    }
  } 
</script>

Note that the itemStore is an export from items-provider module. It's not necessary to use the provide/inject api, you can simply import the itemStore into any component that needs the store, then expose it to the template via data, computed or methods options on the component.

The major advantages to this approach are

  • No extra overhead. We don't have to install an external data management tool. We just have to use tools that already exist within vue
  • The store is read-only meaning it can only be modified my the explicitly defined functions, thereby enforcing one way data flow and eliminating unexpected behaviour.
  • You can structure your store however you want. In the original example, the itemStore has a somewhat flat structure. The computed values and methods are directly on the store. We could just as easily create nested structure to group concerns like so
export const itemStore = readonly({
  state: state,
  getters: {
    totalLength
  },
  actions: {
    addItem,
    removeItem
  }
})

The downside to this approach is that this is not a dedicated data store solution, and because of that, the developer tooling for this approach is not as rich as Vuex (which has a dedicated section in vue devtools and a plethora of plugins).

Conclusion

This is only a base case scenario. For more complex scenarios involving SSR, it might make sense to use provider factories (functions that create or return a new store).

This is purely meant to demonstrate the power that vue provides out of the box and show you another way to store data in your vue application

And finally, this is my first write-up. It took me a while to gather the inertia to write this. Please leave a comment if you found this informative and let me know what you'd like me to write about next. Also i welcome constructive criticism, so don't hold back in the comments 😉.

Discussion

pic
Editor guide
Collapse
jonfleming profile image
Jon Fleming

Thank you. This is the first article I've found that explains Vue shared state with an actual example. It's very difficult to see what needs to be included in different components when looking at the Vue documentation. And the sample application created by vue-cli doesn't use any of these features.