Shared State Management with Vue Composition Api
Tags: vue, vuex, vue3, javascript
Published: 2020-07-20 03:49:23

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.


```javascript
// 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.

```vue
// 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.

```vue
// 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.

```vue
// App.vue

<template>
  <div>
    <items />
    <item-form/>
  </div>
</template>
```
The `App` component composes the `Item` and `ItemForm` components

{% codesandbox global-data-management-with-vuex-example-wib4s module=/src/store.js %}

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

```javascript
// 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

```vue
// 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.

```vue
// 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>

```

```vue
// 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
```javascript
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).

{% codesandbox global-data-management-with-composition-api-kikyi module=/src/items-provider.js %}

## 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 😉.