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, mutation
s mutate the state
, and action
s 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:
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())
}
}
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())
}
}
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
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.
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.
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.
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:
loadOrWait
, for example, something likesetTimeout(()=>loadingPromises[id]=false,60*1000)
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);
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With