Use Vuex to avoid multiple requests from different components

JavaScript is asynchronous, it’s big advantage, but in some cases, also a big problem. The most popular mistakes relate to AJAX requests. Many people trying to do something before there is a response with data, and it cannot work. Now we can use many comfortable tools, and don’t have to remember about that – one of them is async/await. But sometimes, it can be still a problem. Let’s say, we use Vue, have one page with multiple similar components. Each of them needs some data from the same API endpoint, and calls request. In that case, we can’t use async/await, because there a lot of completely separate components. How can we avoid multiple requests then? There are few solutions, one of them is to use Vuex and this post is about that. 

Why don’t use shared property?

Someone can say: wait a moment, you have page, you use a lot of components on it, and every need the same data. Why don’t you set this data as a prop from your parent component? Yes, can I do this, but it also means, that components are much more dependent on each other. It isn’t good practice and we should avoid that, if it’s possible. Each component – as far as possible – should be separate, should have only props for settings and core data. Also, in that case, I prefer to use Vuex to store my data, because it can be used by many, many other components.  

Real usage example: we have users list and we want storing this list in Vuex. Many components on our page can use this list. But before first usage, we must load this list from backend, from our API. If list is used by only one component, it isn’t any issue. We can do this for example using rendered flag or use computed property from store to check, if we can render our component. There is an example with rendered flag: 

    async beforeMount () {
        await this.$store.dispatch('users/loadUsers')
        // component will be visible after loadUsers
        this.rendered = true
    }

    // COMPONENT
    <template>
        <div v-if="rendered">
            (..)

And also, with computed data from the store – there is no call dispatching, because it can be called from other place, other component and we only want wait for data: 

    computed: {
        // Default state for users list should be false or null!
        ...mapState('users', {
            usersList: state => state.list,
        })

    // COMPONENT
    <template>
        <div v-if="users && users.length">
            (..)

We still need other solution

Both solutions are ok, also, both solutions don’t require any other changes… But what can guarantee us, that list will be ready? In the first case, we dispatch store from our component and we know that, it’s ok. But, if we will use many similar components, every of them will also call this dispatch and every will make request to API – it’s bad option. In second case, we don’t know anything about request. Maybe other component, or layout will dispatch for data… or maybe not? It’s also bad option.  

    // Component 1 - it will call request
    async beforeMount () {
        await this.$store.dispatch('users/loadUsers')
    }

    // Component 2 - it will call second request
    async beforeMount () {
        await this.$store.dispatch('users/loadUsers')
    }

    // Component 3 - it will call next request...
    async beforeMount () {
        await this.$store.dispatch('users/loadUsers')
    }

    (...)

The solution is to use first option, but with blocking other requests. How can we achieve that? There is await/async, but if we will call this from other component, code will be separate and also, we don’t know anything about other callings.

Solution: store promise in Vuex!

Solutions is very simple – we can store promise from request in store, and return it, if it’s present. In that case, every other calls will wait for this one promise, for a ready list and will use the same list, when it will be ready. First thing, we must prepare field for this promise in our store state: 

state () {
    return {
        list: [],
        promise: null

And after that, we can modify our action to load list:

async loadUsers ({ state, commit }) {
  if (state.users.length) {
   return state.users
  }  

  if (state.fetchPromise) {
    return state.fetchPromise
  }

  let promise = fetch(usersUrl).then((response) => {
    commit('SET_USERS', response.data)
  })

  commit('SET_PROMISE', promise);
  return promise
}

After this change, code from example above will work in other way:

    // Component 1 - it will call request
    async beforeMount () {
        await this.$store.dispatch('users/loadUsers')
    }

    // Component 2 - it will WAIT for first call!
    async beforeMount () {
        await this.$store.dispatch('users/loadUsers')
    }

    // Component 3 - it will WAIT for first call!
    async beforeMount () {
        await this.$store.dispatch('users/loadUsers')
    }

    (...)

Conclusion

As you can see, we solved a complex problem using a very simple trick – using Vuex to make promise “global” for all modules, that can call the same request. In effect, we will call this dispatch and fetching data from API only once. Number of components with the same code doesn’t matter: only first will call fetching, others will wait for the same result and ready list. Now we can put dispatching data in every place, when we want to have guarantee, that data will be loaded. Without risk, that will loaded never, or will cause multiple requests to the same endpoint.  

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.