VueT2 Dark_1200x303

Vuex is not always the best option to create shareable stateful logic in Vue applications, as only global state should be there. This article covers some alternatives.

There are multiple ways to manage state in Vue applications. This article explores how stateful data and asynchronous operations can be done outside of components without using Vuex.

Event Bus

Event bus is a pattern that uses an empty Vue instance to communicate between components.

// eventBus.js
import Vue from 'vue'

export const bus = new Vue()

// component_one.vue
import { bus } from './eventBus.js'

export default {
    data() {
        return {
            value: null
        }
    }
    method
	created() {
		// Listens for the 'action' event
		bus.$on('action', ({ payload }) => {
            this.value = payload
        })
	}
}

// component_two.vue
import {bus} from './eventBus.js'

export default {
	created() {
		// Emits the 'action' event
		bus.$emit('action', {
			myPayload: 1234
		})
	}
}

Event bus pattern is simple and easy to use, but it does not scale well, especially in medium to large applications with a lot of data flow. Therefore, this pattern could only really be applied in smaller applications. However, I still would not recommend it, as there are better options. What’s more, event bus is deprecated in Vue 3, so if you want to upgrade from Vue 2 faster and easier, forget about the event bus.

State on the Component Level

Most of the time, state can be kept on a component level. If data is needed in child components, then it can be passed as props. But what if we need to share the data between sibling components, or even between component trees on a page?

One way to do that would be to lift the state up to the common ancestor component. This can work if the state is not shared by many components, but if it is, then the data would need to be passed to many components, sometimes deep down the component subtree. Provide/Inject could be used to avoid that, but then good luck with finding out where exactly the data is provided, especially if there are multiple developers working on an app, and it is of a decent size. Therefore, it is not an optimal solution, as some components would only be passing the data through via props and thus be cluttered by data that they do not even use.

Vuex

Vuex is a great library for managing global state in Vue applications, as it was built specifically for that reason by the Vue team. Vuex does introduce a bit of boilerplate and overhead, as we have to learn new concepts. And in addition to state we also have to deal with getters, actions and mutations.

// Vuex module

const state = {
	people: []
}

const getters = {
	getPeople(state) {
		return state.people
	}
}

const actions = {
	async fetchPeople(context, payload) {
		const response = await axios.get("https://swapi.dev/api/people/")
		context.commit('SET_PEOPLE', response.data.results)
	}
}

const mutations = {
	SET_PEOPLE(state, payload) {
		state.people = payload
	}
}

export default {
	state,
	getters,
	actions,
	mutations
}
// A component
<script>
import {mapActions, mapGetters} from 'vuex'

export default {
	computed: {
		...mapGetters(['getPeople'])
	},
	methods: {
		...mapActions(['fetchPeople'])
	},
	created() {
		this.fetchPeople()
	}
}
</script>

Nevertheless, not all the state that has to be shared between components should land there. I’ve seen many applications where data that isn’t shared in many places was moved to a global store. What’s more, Vuex store is exposed to every component in an application, so its actions and mutations can be accessed from anywhere. However, if we would like to encapsulate stateful logic to a specific feature or a subtree of components, we can’t do it with Vuex. Fortunately, there are other ways of sharing stateful data.

Vue.observable

Vue.observable was added to Vue in version 2.6. It allows us to create a reactive object of which changes are tracked, and Vue components would re-render accordingly. We can create a new file and create a reactive state outside of components. Then we import exactly what we need.

// service.js
import Vue from 'vue'

const initialState = {
	people: []
}

// Reactive state
export const state = Vue.observable(initialState)

// Fetch people and update the state
export const fetchPeople = async () => {
	const response = await axios.get("https://swapi.dev/api/people/")
	state.people = response.data.results
}

// components
import {state, fetchPeople} from './service'

export default {
	computed: {
        // Get people from the external state
		people() {
			return state.people
		}
	},
	created() {
        // Initialise api call
		fetchPeople()
	}
}

We can go even further and use a computed prop with a getter and setter to make it easier to retrieve and update the state.

// service.js

// ... the rest of the code
export const peopleComputed = () => ({
	people: {
		get() {
			return state.people
		},
		set(value) {
			state.people = people
		}
	}
})
<!-- component -->
<template>
  <div id="app">
    <div v-for="(person, index) in people" :key="index">{{person.name}}</div>
  </div>
</template>

// components
<script>
import {state, peopleComputed, fetchPeople} from './service'
export default {
	// other code
	computed: {
        // Spread the computed prop
		...peopleComputed()
	}
	// other code
	created() {
        // This works, as the people computed property has a setter
		this.people = [{
			name: 'Thomas'
		}]
	}
}
</script>

As the example above shows, we have reduced the amount of code needed for shareable stateful logic and avoided concepts of actions and mutations. Instead, we have just one function that can fetch data and update the state immediately. What’s more, this state is only available wherever it is imported, so it is nicely encapsulated only to components that require it.

Vue.observable is a pattern I use in Vue 2. However, in Vue 3 there is something even better for that—Composition API.

Composition API

Composition API is a new feature in Vue 3. Inspired by React Hooks, it allows us to easily create reusable stateful logic. It can also be used in Vue 2 applications via a plugin—vuejs/composition-api. I won’t be getting into details about how Composition API exactly works, as I have already written an article about it, which you can find here.

If you know how it works, then that’s great! Let’s get to the implementation.

// service.js

// vue 2
// import { ref } from "@vue/composition-api";

// vue 3 
import { ref } from 'vue'
import axios from "axios";

export const usePeople = () => {
  // Reactive state
  const people = ref([]);
	
  // Fetch data and set it on state
  const fetchPeople = async () => {
    const response = await axios.get("https://swapi.dev/api/people/");
    people.value = response.data.results;
  };
  
  // Return state and the fetchPeople method
  return {
    people,
    fetchPeople
  };
};
<script>
import { usePeople } from "./service";
    
export default {
  name: "App",
  components: {},
  setup() {
    // Initialise usePeople
    const { people, fetchPeople } = usePeople();
    // Called immediately, equivalent of "created"
    fetchPeople();
      
    /*
    	You can also update people ref directly like so
    	people.value = [{name: 'Thomas'}]
    */

    // Return the state
    return {
      people
    };
  }
};
</script>

The code written using Composition API is even more clear and concise than previous examples, and personally, I will be using it in Vue 3 a lot.

Conclusion

There are multiple ways to manage state in Vue applications. It is important to consider all the options before jumping straight to Vuex, as it might not always be the best choice, especially if the data we are dealing with does not have to be global. Event bus should not be used anymore, as it is deprecated. Instead, consider using Vue.observable or Composition API.


Thomas Findlay
About the Author

Thomas Findlay

Thomas Findlay is a web and mobile developer, mentor, technical writer and consultant with almost six years of experience. He specializes in frontend web technologies including Vue.js, React.js, React Native for mobile applications, along with backend: PHP, Laravel, Python, Flask, Node.js and Express.js. Thomas has designed and developed websites and mobile applications for individuals and small and large businesses. Learn more about Thomas at https://www.codementor.io/thomas478

Related Posts

Comments

Comments are disabled in preview mode.