Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best practice for fetching data from api using vuex

Tags:

vue.js

vuex

There are two pages (or, components with respect to vue's terminology) who both needs same set of data, which is provided via an api over http. The order in which those two components would be visited is undefined (or, user-behavior-dependent) and the data should only be fetched once since it won't change alot.

I am aware of the idea that a state stores the actual data, mutations mutate the state, and actions do the dirty works as async requests, multi-mutation coordinating, etc.

The question is: What is the best practice to do some caching logic as described above ?

I have come up with following three ways but none of them looks perfect to me:

Cons:

  1. I need to dispatch the action before accessing the data at everywhere because I do not know if the data has already been fetched.

    // ComponentA
    async mouted () {
        await this.store.dispatch('fetchData')
        this.someData = this.store.state.someData
    }
    
    // ComponentB
    async mouted () {
        await this.store.dispatch('fetchData')
        this.someData = this.store.state.someData
    }
    
    // vuex action
    {
       async fetchData ({ state, commit }) {
           // handles the cache logic here
           if (state.someData) return
           commit('setData', await apis.fetchData())
       }
    }
    
  2. Caching logic is scattered all over the code base -- smelly~

    // ComponentA
    async mouted () {
        if (this.store.state.someData === undefined) {
            // handles the cache logic
            await this.store.dispatch('fetchData')
        }
        this.someData = this.store.state.someData
    }
    
    // ComponentB
    async mouted () {
        if (this.store.state.someData === undefined) {
            // handles the cache logic
            await this.store.dispatch('fetchData')
        }
        this.someData = this.store.state.someData
    }
    
    // vuex action
    {
       async fetchData ({ state, commit }) {
           commit('setData', await apis.fetchData())
       }
    }
    
  3. Maybe the most prefect-looking among these three, but I feel a little strange to use a return value of an action dispatch as data. And the caching logic would be scattered all over the actions as the store grows (there would be more and more actions repeating same caching logic)

    // ComponentA
    async mouted () {
        this.someData = await this.store.dispatch('fetchData')
    }
    
    // ComponentB
    async mouted () {
        this.someData = await this.store.dispatch('fetchData')
    }
    
    // vuex action
    {
       async fetchData ({ state, commit }) {
           if (!state.someData) commit('setData', await apis.fetchData())
           return state.someData
       }
    }
    

I perfer to put the caching logic into my 'vue&vuex-independent' network layer. But then the 'caching' part of the network layer may become another 'vuex' store. XD

like image 203
Nandin Borjigin Avatar asked Jul 01 '18 08:07

Nandin Borjigin


People also ask

How do I fetch API in Vue?

Vue Fetch data from API exampleconst responsePromise = fetch(resourceUrl [, options]); The Response object we mention above represents the entire HTTP response, it does not directly contain the response body. To get the actual JSON body of the response, we use response. json() method.

Should I use pinia or Vuex?

Pinia has a Simpler API than Vuex Pinia's API is simpler and more intuitive than Vuex. Getting started with state management is much easier even for a junior developer as a lot of boilerplate code that needed to be written between every state change in Vuex has now been removed in Pinia.

Is Vuex getter cached?

Vuex allows us to define "getters" in the store. You can think of them as computed properties for stores. As of Vue 3.0, the getter's result is not cached as the computed property does.


2 Answers

i feel like i'm reinventing wheels and there should be something better off-the-shelf to do this, but here is my wheel and it rolls ok for me.

i am replying to give back, but also as i'd love someone to tell me a better way!

my approach solves the issue of avoiding multiple fetches by using a 'loading' promise instead of simply checking if a particular state is loaded. if your api is slow and/or your components might call the fetch action several times before a render, this will help you keep it to a single api call.

  // vuex action
  async fetchCustomers({commit, state}) {
    await loadOrWait(loadingPromises, 'customers', async () => {
      // this function is called exactly once
      const response = await api.get('customers');
      commit('SET_CUSTOMERS', {customers : response.data });
    });
    return state.customers;
  },

loaded is just an object which stores a promise for every fetch. i've seen someone use a weak map instead (and that would be a good choice if memory use is a concern)

  const loadingPromises = { 
    customers: false,
    customer: { }
  }

loadOrWait is a simple utility function that either executes (exactly once) a function (eg, your fetch from an api) or it sees a fetch is in process, so it returns the promise of the previous fetch call. in either case, you get a promise which will resolve to the result of the api call.

async function loadOrWait(loadingPromises, id, fetchFunction) {
  if (!loadingPromises[id]) {
    loadingPromises[id] = fetchFunction();
  }
  await loadingPromises[id];
}

perhaps you want to be more granular in your fetch, for example, eg fetch a specific customer if they have not been fetched yet.

  // vuex action 
  async fetchCustomer({commit, state}, customerId) {
    await loadOrWait(loadingPromises.customer, customerId, async () => {
      const response = await api.get(`customers/${customerId}`);
      commit('SET_CUSTOMER', { customerId, customer: response.data });
    });
    return state.customer[customerId];
  },

some other ideas:

  • maybe you don't want to cache the results forever, in which case you could get creative inside loadOrWait, for example, something like
setTimeout(()=>loadingPromises[id]=false,60*1000)
  • perhaps you want to refresh/poll data at some regular interval. pretty simple to have loadOrWait save the functions as well! when you execute them, it will update the state and if you've been a good vue coder, your application will refresh.
const fetchFunctionMap = {};

async function loadOrWait(loadingPromises, id, fetchFunction) {
  if (!loadingPromises[id]) {
    loadingPromises[id] = fetchFunction();
    // save the fetch for later
    fetchFunctionMap[id] = fn;
  }
  await loadingPromises[id];
}

async function reload(loadingPromises, id) {
  if (fetchFunctionMap[id]){
     loadingPromises[id] = fetchFunctionMap[id]();
  }
  return await loadingPromises[id];
  // strictly speaking there is no need to use async/await in here.
}

//reload the customers every 5 mins
setInterval(()=>reload(loadingPromises,'customers'), 1000 * 60 * 5);

like image 103
Tom Carchrae Avatar answered Sep 27 '22 18:09

Tom Carchrae


I came across a similar issue lately. And figured out, it'll be best to commit anyway inside your action. This helps to stick to the vuex lifecycle. So your code might look like this:

{
    async fetchData ({ state, commit }) {
       if (!state.someData) commit('setData', await apis.fetchData())
       commit('setData', state.someData)
    }
 }

Then use getters to work with state.someData in your component instead of assigning it.

like image 40
Thomas Rutzer Avatar answered Sep 27 '22 17:09

Thomas Rutzer