Best practice for fetching data from api using vuex
Asked Answered
M

3

7

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

Mercurate answered 1/7, 2018 at 8:34 Comment(0)
M
2

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);

Malpighi answered 28/4, 2021 at 0:54 Comment(0)
I
1

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.

Ibanez answered 11/9, 2018 at 13:51 Comment(0)
B
0

I know I came here pretty late but I hope my approach may bring up a potential solution for this issue. This approach is actually what I've been using across many projects and I found it's quite effective and easy to follow.

My idea is basically using an API factory/service instead of Vuex action. Every time you need to get data, you call a service that is exposed from the API factory. This service will check if the data you need is already in the store, otherwise retrieving it from the API.

It goes like this:

// Vuex store
const store = new Vuex.Store({
  state: {
    someData: {}
  },
  mutations: {
    SET_SOME_DATA (state, payload) {
      state.someData = payload
    }
  }
})

// API factory/service
import store from 'yourStoreDirectory'

export async getSomeData() {
  if (store.state.someData !== undefined) {
    return store.state.someData
  }

  try {
    // fetch API

    // store data into Vuex for next time
    store.commit('SET_SOME_DATA', response)
    return response
  } catch (err) {
     // handle errors
  }
}

// Component
import { getSomeData } from 'yourAPIFactoryDirectory'

export default {
  async mounted () {
    this.someData = await getSomeData()
  }
}

So every time you use this getSomeData service, you don't have to think about whether it's from the store or API fetching. This way makes your code cleaner and more readable.

I personally don't use action that much and in general, I always try keep my Vuex as much simple as possible, just like its title - state management, not data processing or business logic management. You can refer to this article to read more about this idea, https://javascript.plainenglish.io/stop-using-actions-in-vuex-a14e23a7b0e6

Brunel answered 1/9, 2021 at 10:40 Comment(1)
you probably wanted to return store.state.someData after fetching it from API.Beeck

© 2022 - 2024 — McMap. All rights reserved.