Table of contents
Caching API calls is one of the most important aspects of a life of a frontend developer. And as you grow in your career, you must know some caching techniques to make your website more performant.
Here, I am going to explain one very simple way to add a basic caching while making API calls. The main concept I am going to use here is "JavaScript Closures".
Detour - Understanding Closures
When we see or listen closures, we think of it as something very complicated, but it actually is very simple. It just returns another function which has the context of the parent function even after the parent function has stopped executing. For example:
const parentFn = (firstName, lastName) => {
// concatinating firstName and lastName to form fullName
let fullName = `${firstName} ${lastName}`;
// returning an anonymous function
return () => {
// printing the `fullName` variable from parent function
console.log(fullName);
}
}
// executing the parentFn and storing the response in a variable `getFullName`
// since parentFn returns a function, `getFullName` will be a function
const getFullName = parentFn('Shubhaw', 'Kumar');
// executing the `getFullName` function
getFullName(); // prints "Shubhaw Kumar"
In the above example, the parent function returns a child function. Now, when this child function getFullName
is called, it prints the value of fullName
variable. But notice that the fullName
was a variable of the parentFn
which has already finished executing. And we know when a function stops executing, its variables are also thrown out of the execution context. But since the parent function returns a child function, the child function still keeps the variables of parent function in its own execution context and thus, can access those variables. Hence, in our example, the child function is able to successfully print the correct value of fullName
variable.
Back to the main topic
Now, using the same concept we discussed above, we are going to implement a simple caching mechanism in our code below. First let's look at the code, and then I'll give the explanation:
// Implementing the caching function
const cacheWrapper = (timeInMs) => {
let cache = {}; // initializing with an empty object
return async (url, config) => {
// key to identify a unique combination of url & config
const key = `${url}_${JSON.stringify(config)}`;
// fetching the entry for the above key from the cache
const entry = cache[key];
// if entry is not present, or the existing entry has expired, make the API again
if (!entry || Date.Now() > entry.expiryTime) {
// using try-catch is a good practice to make sure your UX is not breaking
try {
console.log("Fetching new data");
// calling the API again
let response = await fetch(url, config);
response = await response.json();
// storing the result in the cache object for the `key`
cache[key] = {
data: response,
expiryTime: Date.Now() + timeInMs,
};
} catch (error) {
// either log the error on the console
// or handle it gracefully like showing a pop-up, etc.
console.error("Error fetching data");
}
}
// return the result from the cache
return cache[key];
}
}
// calling the cacheWrapper to get cached API caller
const fetchWithCache = cacheWrapper(5000);
// fetching the API result
fetchWithCache("https://jsonplaceholder.typicode.com/posts/1", {
method: "GET",
}).then((data) => console.log("Without timeout:", data));
// fetching the response within 1 sec
// this will return the cached result
setTimeout(() => {
fetchWithCache("https://jsonplaceholder.typicode.com/posts/1", {
method: "GET",
}).then((data) => console.log("With 1 sec timeout:", data));
}, 1000);
The above code is very similar to the one I explained in the closure example. Instead of the parentFn
, the name here is cacheWrapper
. Instead of the fullName
variable, we are using cache
variable which is of object type and instead of the 1 liner anonymous function, we are using a little complex async
function here.
This cacheWrapper
function accepts timeInMs
argument which is used to invalidate the cache, i.e., once the given amount of time passes, we should no more return the cached result, instead we should make an actual call to the API. This is a necessary practice so that there's we do not end of showing any stale data.
In this async
function, we first define a key
using the url
and config
provided to us. This is our unique identifier based on which we can distinguish between the different API calls. Using this key
we update the cache
object. You are free to choose any object structure, but, for simplicity, I have just kept two properties in the object - the api response data
and the expiryTime
. This expiryTime
uses the timeInMs
argument's value to calculate the time after which the particular cache value should be invalidated.
Now, based on this key
, we try to check if there's an entry for the key
already in the cache
object or not. If not, this means for the particular combination of url
and config
no API call has been made yet. So, we make the API call and put the response in the cache and return the data back as well. And, we do the exact same thing in cases where an entry is there for a particular key but the time has expired, so we again make another API call and replace the older cache value with the new one. As mentioned earlier, this makes sure that our data has never gone stale.
Now, you can get a cached API caller by executing the cacheWrapper
. Here, I have called it as fetchWithCache
. Using this fetchWithCache
you can make the API calls, frequently and it will make the actual API calls only when required.
That's all about it. Let me know if you have any kind of questions regarding this either here or over my LinkedIn.