Caching network requests on the frontend
Tags: javascript, codenewbie, react, webdev
Published: 2020-03-21 10:30:25

  
## Hello people!  
###### First post on dev.to!

I'm Harsh. I'm a learning full stack developer trying to gain knowledge.  
Today I'll be learning with you, how to cache network requests on the frontend.  
The code discussed here is available on Github as [api-cache-example](https://github.com/nosyminotaur/api-cache-example "Just click on it!").  
I was writing a small app where I was fetching some timestamps from my own backend. This was a medium sized app, composed of React and Redux, written in Typescript. I was using axios as my HTTP Client.  
*(Just a side note, this code is in Typescript, but can be easily extended to Javascript by following similar ideas.*)  
I really really wanted to cache my requests on the client, so that I didn't have to make repeated calls to my API.
I thought of a simple solution, and started implementing it, using [interceptors](https://github.com/axios/axios#interceptors "Axios Interceptors").
The idea was very simple. Have a cache, that can store objects of any type. Then invalidate them if they've been stored for a period longer than the caching period.  
Simple, right?
Let's implement it, then!  
First, we will be creating the cache.
We will create a file named `cacheHandler.ts`.
What should we have here?
Let's think logically. The cache must handle two requests ->
1. store.
2. retrieve if valid.

So let's make two functions, `store()` and `isValid`.
```typescript
function store(key: string, value: string) {
    const finalValue = `${value}${SEPARATOR}${Date.now().toString()}`;
    localStorage.setItem(key, finalValue);
}
```  
```typescript
function isValid(key: string): IsValidResponse {
    const value = localStorage.getItem(key);
    if (value === null) {
        return {
            isValid: false,
        };
    }
    const values = value.split(SEPARATOR);
    const timestamp = Number(values[1]);
    if (Number.isNaN(timestamp)) {
        return {
            isValid: false,
        };
    }
    const date = new Date(timestamp);
    if (date.toString() === 'Invalid Date') {
        return {
            isValid: false,
        };
    }
    if ((Date.now() - date.getTime()) < CACHE_INTERVAL) {
        return {
            isValid: true,
            value: values[0],
        };
    }
    localStorage.removeItem(key);
    return {
        isValid: false,
    };
}
```

If you look carefully, `isValid` returns a response of type `IsValidResponse`, which is shown below:
```typescript
interface IsValidResponse {
    isValid: boolean,
    value?: string,
}
```
We are missing the constants, so let's add that:
```typescript
const SEPARATOR = '//**//';
const CACHE_INTERVAL = 0.2 * 60 * 1000;
```

`store()` is a very simple function that takes a string, adds a separator and the current date after that and stores it in localStorage. This allows `isValid()` to retrieve the data and the date by splitting on the separator.
Now we need to check if the date is not invalid or not expired, and we can send a boolean that tells the caller that the cache is yet not invalidated, and we can use it.
Now, what should we use as the *key* for storing the object in localStorage?
We'll answer that soon.  
You can refer to the file directly, [here](https://github.com/nosyminotaur/api-cache-example/blob/master/src/cacheHandler.ts).  
Now, onto the axios client.
We first create a client:
```typescript
export const client = axios.create({ baseURL: 'http://localhost:8080/api/widget', withCredentials: true });
```
`baseURL` can be anything, based on where you want to send a request.
I have a server at port 8080 that returns a JSON object with today's weather, but you can use any API, really.  
Now we add the interceptors:
```typescript
client.interceptors.request.use((request) => requestHandler(request));
client.interceptors.response.use(
    (response) => responseHandler(response),
    (error) => errorHandler(error),
);
```

```typescript
const whiteList = ['weather'];

function isURLInWhiteList(url: string) {
    return whiteList.includes(url.split('/')[1]);
}

function responseHandler(response: AxiosResponse<any>): AxiosResponse<any> {
    if (response.config.method === 'GET' || 'get') {
        if (response.config.url && !isURLInWhiteList(response.config.url)) {
            console.log('storing in cache');
            cache.store(response.config.url, JSON.stringify(response.data));
        }
    }
    return response;
}

function errorHandler(error: any) {
    if (error.headers.cached === true) {
        console.log('got cached data in response, serving it directly');
        return Promise.resolve(error);
    }
    return Promise.reject(error);
}

function requestHandler(request: AxiosRequestConfig) {
    if (request.method === 'GET' || 'get') {
        const checkIsValidResponse = cache.isValid(request.url || '');
        if (checkIsValidResponse.isValid) {
            console.log('serving cached data');
            request.headers.cached = true;
            request.data = JSON.parse(checkIsValidResponse.value || '{}');
            return Promise.reject(request);
        }
    }
    return request;
}
```  
Whew, a lot of code just ran past!
First, let's look at `isURLInWhiteList`. This is just so that we can blacklist some URLs to not be stored in the cache. This might be used with authentication routes.
Now, onto the `responseHandler`.
The first if is used to check if a `GET` request was made.
```typescript
if (response.config.method === 'GET' || 'get')
```
If yes, then is the url not in the whitelist?
```typescript
if (response.config.url && !isURLInWhiteList(response.config.url))
```
If these conditions are met, simply store the object in the cache with the key as the **URL** of the request.
Now we'll work on the `requestHandler` first.
The first if is used to check if a `GET` request was made.
```typescript
if (response.config.method === 'GET' || 'get')
```
Then check if cache was valid
```typescript
const checkIsValidResponse = cache.isValid(request.url || '');
if (checkIsValidResponse.isValid) 
```
If yes, this means that the cache is still valid and we can just serve that instead of sending a response!  
So, add a header to the request, named `cached`(*it could be anything, this is my personal preference*), and set it to true.
```typescript
request.headers.cached = true;
```
Set the request data here only to the cache
```typescript
request.data = JSON.parse(checkIsValidResponse.value || '{}');
```
and then, **Promise.reject** the request.
Why?  
This is done because then this request get sent to the `errorHandler` immediately. Here, we can just check if we have a `cached` header. If yes, this means that the data is cached, and not a real error. Else, we could just reject the error.
So that is what we do.
```typescript
function errorHandler(error: any) {
    if (error.headers.cached === true) {
        console.log('got cached data in response, serving it directly');
        return Promise.resolve(error);
    }
    return Promise.reject(error);
}
```
If the cached header is present, we return a *Promise.resolve* so that axios treats it like no error had ever occurred and we get this data inside the `.then` instead of the `.catch`. So the `get` caller never knew that caching was happening behind the scenes!  
And if it is any other error, just return a *Promise.reject* instead, so that it behaves like a normal error! Now isn't that smart?  
I used this design in a React app that looked like the following:  
![Cache improvements](https://raw.githubusercontent.com/nosyminotaur/api-cache-example/master/cache.gif)

1604 ms to a whopping **3ms**.
That is **535** times faster than the non-cached version.
And by changing the constant `CACHE_INTERVAL`, we can modify how long the cache should stay validated.  
You can checkout the project at [my GitHub account](https://github.com/nosyminotaur/api-cache-example "Just click on it!")  

One last question before leaving. How do I use this with [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)?
Well, some questions should be left to the reader to answer themselves. Otherwise, what's the purpose of learning?