The other day, I found myself searching for a solution whilst working on my Vue app. I had multiple components which were relying on the same data from an API, and when used together, were making duplicate requests to the same endpoint. After some head-scratching and experimenting, I pieced together a solution that I thought may be helpful to others facing a similar challenge.
In this article, I’ll outline the scenario, explain the challenges, and walk you through the code that helped me prevent those duplicate API calls. Whether you’ve encountered this problem yourself or just want to build up your Vue toolkit, this guide aims to provide a practical solution to this common issue.
The Scenario
Imagine you have several Vue components that depend on the user’s profile data from an API. As components, these can function independently or be used simultaneously on the same page. If each component has a call to load the data in its onMounted()
callback, you'll end up with simultaneous requests to the same endpoint - one for each component when they are used together.
This flood of simultaneous requests can lead to several issues:
Performance Impact: Multiple identical requests can strain the client and the server, potentially leading to slower response times.
Redundant Data Retrieval: Since all the simultaneous requests are fetching the same data, the multiple calls simply overwrite the same local data each time, resulting in redundant operations that waste resources.
Wasted Resources: Making redundant requests wastes bandwidth and processing power, both on the client and server sides.
The Challenge
When faced with multiple components relying on the same API data, two main challenges emerged that required careful consideration:
Independent Component Inclusion: Each component should be able to include and load data independently without relying on a parent component to fetch the required information. If the components are used independently without the parent that contains the loading logic, they would not be able to fetch the required data. This means the responsibility of data fetching must be inside the components themselves, allowing them to function correctly whether used alone or together with others.
Avoiding Duplicate Requests: When multiple components are used simultaneously, duplicate API requests must be prevented. Whilst there are a number of ways to handle this, I wanted to allow the first request to continue and succeed, but all subsequent requests, whilst the first is still pending, to simply wait for the initial request to complete. This ensures that the system doesn’t waste resources on redundant requests for the same data, and it also helps to maintain consistency as all components relying on this data will receive the exact same information once the initial request is fulfilled.
The Solution
Here’s the code snippet that addresses this challenge using a Pinia setup store with the Composition API (as I prefer):
import { defineStore } from 'pinia';
import { ref } from 'vue';
import apiClient from 'boot/axios';
export const useProfileStore = defineStore('profile', () => {
const profile = ref(null);
let loadProfileRequest = null;
const loadProfile = async () => {
if (loadProfileRequest) {
return loadProfileRequest;
}
loadProfileRequest = new Promise(async (resolve, reject) => {
try {
profile.value = (await apiClient.get('/v1/user/profile')).data;
resolve();
}
catch (err) {
reject(err);
}
finally {
loadProfileRequest = null;
}
});
return loadProfileRequest;
};
return {
profile,
loadProfile
};
});
How the Code Works
Initialisation: The code snippet defines a store named ‘profile’ that includes a reactive reference
profile
to hold the user's profile data, and a variableloadProfileRequest
to keep track of the ongoing API request.Checking for an Ongoing Request: Inside the
loadProfile
function, it checks if there's an ongoing request stored inloadProfileRequest
. If so, it returns this same request, allowing other components to await its completion rather than initiating a new one.Creating and Storing the Request: If no request is in progress, a new Promise is created for the API request, and the reference is stored in
loadProfileRequest
. Any subsequent calls toloadProfile
from other components will return this request instead of initiating a new one.Making the API Call: Inside the Promise, the actual API call is made, and the result is assigned to
profile.value
.Resolving or Rejecting the Promise: Depending on the success or failure of the API call, the Promise is either resolved (with no value in this case) or rejected with an error.
Cleaning Up: In the
finally
block, the ongoing request variable is reset. This ensures that subsequent calls toloadProfile
will create a new request if needed.Updating Components: Once the API request has completed, the reactive
profile
ref will be updated with the data. As a result, the updated data will become available within the components that use this store. There's no need for individual components to manage the request or its state; they can simply consume theprofile
value as it updates.
This pattern provides a flexible and efficient way to manage shared API data across multiple Vue components. By centralising the loading logic and carefully managing ongoing requests, you can ensure that components are self-sufficient in fetching the data they need, while also preventing them from redundantly requesting the same data when used together.
By leveraging Vue’s reactivity system, the components can simply observe the shared profile
value and react to updates, without needing to be concerned with the underlying loading mechanism. This encapsulation of concerns helps to create a clean and maintainable codebase.
Thank you for reading along and I hope you have found this solution helpful.
Got another solution to the same problem which we can add to the Vue toolkit? Drop it in the comments!
Happy coding!