How to implement Caching for Hacker News App in React
Tags: javascript, react, codenewbie
Published: 2021-02-18 06:16:53

In this article, we will implement a caching mechanism for the [Hacker News Clone](https://hackernews-clone-react-app.netlify.app/top) App which is explained in detail in [this freeCodeCamp article](https://www.freecodecamp.org/news/how-to-build-a-hacker-news-clone-using-react/).

You can find the complete GitHub source code for the Hacker News Clone App in [this repository](https://github.com/myogeshchavan97/hackernews-clone-react-app).

In this article, you will learn:

* How to implement caching in React
* How to approach fixing the bugs
* How to change the API response data
* ES6 destructuring syntax changes

and much more. 

So let's get started.

## Need of implementing caching

If you check the application [live demo](https://hackernews-clone-react-app.netlify.app/top), you will notice that when we click on any of the `top stories`, `latest stories` or `best stories` link in the navigation, we're showing a loading message for some time while the response is coming from the API and once we receive the response, we're hiding the loading message and showing the response data.

![Hacker News Clone demo](https://gist.github.com/myogeshchavan97/aa75611665802aadfd3ba6bfeb0fe59b/raw/06ff931efc03ef42cd70a8a44c0dd211a53f5a59/loader.gif)

The application is working correctly and there is no issue with it. But we can improve it further by adding caching functionality.

When we first time clicks on any of the navigation links, we're loading the list of the first 30 news from the Hacker News API related to that type of story(`top`, `latest` or `best`) as shown below inside the `utils/apis.js` file.

```js
export const getStories = async (type) => {
  try {
    const { data: storyIds } = await axios.get(
      `${BASE_API_URL}/${type}stories.json`
    );
    const stories = await Promise.all(storyIds.slice(0, 30).map(getStory));
    return stories;
  } catch (error) {
    console.log('Error while getting list of stories.');
  }
};
```

But If we again click on any other story type(`top`, `latest` or `best`), we again get the loading message as the API call is made again because inside the `useDataFetcher.js` file, we have added a `useEffect` hook which makes API call every time the `type` changes.

> If the API data is frequently changing within seconds then there is no need of implementing caching which we're about to see because we always want up-to-date data.

But in our Hacker News API, the data does not change quite frequently and it may not be a good user experience to load the data, again and again, every time we click on any `type` as the user have to wait for the response to come before it gets displayed.

## Implementing Caching

We can fix this issue by caching the data once we receive it from the API. So the next time we click on any of the navigation links, we check If the data is already present in the cache(state in React) and make the API call only if it's not present otherwise we will load the same data which is present in the state.

To get started clone the repository code from [this URL](https://github.com/myogeshchavan97/hackernews-clone-react-app.git).

Once cloned, install the npm dependencies by executing the `yarn install` command from the terminal/command prompt and start the application by executing the `yarn start` command.

Now, If you open the `hooks/dataFetcher.js` file, you will see that we're storing the list of stories coming from the API in a state with the name `stories` as shown below:

```js
const [stories, setStories] = useState([]);
...

useEffect(() => { 
  ...
 setStories(stories);
 ...
});
```

So every time the response comes from the API, we're updating the `stories` array with that data.

Instead of storing the `stories` in an array, we will store them in an object in the following format:

```js
const [stories, setStories] = useState({
   top: [],
   new: [],
   best: []
});
```

So `stores.top` will contain the top stories, `stories.new` will contain the latest stories and `stories.best` will contain the best stories.

To start with, we will initialize the stories array with an empty object like this:

```js
const [stories, setStories] = useState({});
```

Now, replace your `useEffect` hook with the following code:

```js
useEffect(() => {
  if (!stories[type]) {
    setIsLoading(true);
    getStories(type)
      .then((stories) => {
        console.log('stories', stories);
        setIsLoading(false);
      })
      .catch(() => {
        setIsLoading(false);
      });
  }
}, [type]);
```

In the above code, we have added an if condition, so only when there is no already loaded `top`, `new` or `best` story inside the `stories` object, we will make an API call. 

`!stories[type])` is same as saying `stories[type]` does not exist or is `null` or `undefined`.

We also added a `console.log` statement once inside the `.then` handler so we can check how the stories array looks like.

> Remember that we initially set the `stories` state to empty object `{}` so initially there will not be any `top`, `new` or `best` stories inside the `stories` object.

And now, instead of exporting `story` from the hook, we need to export the selected `type` of the story as `story` is an object now and `story[type]` is an array.

So change the below code:

```js
return { isLoading, stories };
```

to this code:

```js
return { isLoading, stories: stories[type] };
```

Your entire `dataFetcher.js` file will look like this now:

```jsx
import { useState, useEffect } from 'react';
import { getStories } from '../utils/apis';

const useDataFetcher = (type) => {
  const [stories, setStories] = useState({});
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    if (!stories[type]) {
      setIsLoading(true);
      getStories(type)
        .then((stories) => {
          console.log('stories', stories);
          setIsLoading(false);
        })
        .catch(() => {
          setIsLoading(false);
        });
    }
  }, [type]);

  return { isLoading, stories: stories[type] };
};

export default useDataFetcher;
```

Now, If you run the application by executing `yarn start` command, you will see the following screen:

![Stories Error](https://gist.github.com/myogeshchavan97/aa75611665802aadfd3ba6bfeb0fe59b/raw/ee29ee17105203648d7abef0f5385526edff732a/stories_error.png)

We're getting the error in the `ShowStories.js` file where we're using the map method. This is because, initially when the application is loaded, the `stories` state in the `useDataFetcher.js` file is an empty object and so `stories[type]` will be `undefined`. 

Therefore when we use the `stories.map` method, it produces an error because `map` can be used only for arrays and not for `undefined`.

So to fix this, we need to initialize the `stories` to be an empty array in the `ShowStories.js` file. 

Therefore, change the below code:

```js
const { isLoading, stories } = useDataFetcher(type ? type : 'top');
```

to this code:

```js
const { isLoading, stories = [] } = useDataFetcher(type ? type : 'top');
```

Here we're using ES6 destructuring syntax for assigning a default value of an empty array to the `stories` variable.

So as `stories` is an empty array initially, `stories.map` will not give an error.

Now, If you check the application, you will see the following screen:

![Stories log](https://gist.github.com/myogeshchavan97/aa75611665802aadfd3ba6bfeb0fe59b/raw/78d535337f63af56eb00ccebcb37d0028057cec6/stories_log.png)

As we have added the `console.log` statement inside the `dataFetcher.js` file at line 13, you can see the list of `stories` we got from the API response.

Now, we got the `stories` from the API, we need to call the `setStories` function to set the `stories` inside the `.then` handler of the `dataFetcher.js` file so we can see the list of `stories` on the screen.

If you remember, our `stories` object will look like this once its populated with stories:

```js
const [stories, setStories] = useState({
   top: [],
   new: [],
   best: []
});
```

And as in React Hooks, in the case of the object, the state is not merged automatically but we need to manually merge it. Check out my [this article](https://levelup.gitconnected.com/an-introduction-to-react-hooks-50281fd961fe?source=friends_link&sk=89baff89ec8bc637e7c13b7554904e54) to understand it better.

So inside the `dataFetcher.js` file, replace the `console.log` statement with the following code:

```js
setStories((prevState) => {
  return {
    ...prevState,
    [type]: stories
  };
});
```

Here, we're using the updater syntax of setState along with the ES6 dynamic key syntax for the object, so we're first spreading out the `stories` object and then adding the selected `type` with the `stories` array.

As we're returning just an object from the function, we can further simplify it to the below code where we're implicitly returning the object from the function:

```js
setStories((prevState) => ({
  ...prevState,
  [type]: stories
}));
```

Your entire `dataFetcher.js` file will look like this now:

```jsx
import { useState, useEffect } from 'react';
import { getStories } from '../utils/apis';

const useDataFetcher = (type) => {
  const [stories, setStories] = useState({});
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    if (!stories[type]) {
      setIsLoading(true);
      getStories(type)
        .then((stories) => {
          setStories((prevState) => ({
            ...prevState,
            [type]: stories
          }));
          setIsLoading(false);
        })
        .catch(() => {
          setIsLoading(false);
        });
    }
  }, [type]);

  return { isLoading, stories: stories[type] };
};

export default useDataFetcher;
```

Now, If you check the application, you will see the following screen:

![Instant Loading](https://gist.github.com/myogeshchavan97/aa75611665802aadfd3ba6bfeb0fe59b/raw/d2fc75049a70d083f68598dd2e92b566281d4d59/instant_loading.gif)

As you can see in the above gif, when we first time clicks on the `top`, `latest` or `best` stories link, we get the loading message but once the content is loaded, the second time we click any of the links, the data is loaded instantly without the loading message because we're not making an API call as data is already present in the state because the data was already loaded in the first time click.

**That's a great improvement to the application and with this, we're done with adding caching functionality to the application.**

> One thing you should remember is to use this caching technique only if the API data is not frequently changing otherwise you might see the old data until you refresh the page because we're not making an API call to get the updated data If the data is already loaded once.

## Couple of optional code improvements

As seen previously, our `stories` array looks like this:

![Stories log](https://gist.github.com/myogeshchavan97/aa75611665802aadfd3ba6bfeb0fe59b/raw/78d535337f63af56eb00ccebcb37d0028057cec6/stories_log.png)

Each array element is an object with properties like `config`, `data`, `headers`, `request` etc.
Out of these, only the `data` property is usable property. We're getting these extra properties because we're directly returning the story from the `getStory` function.

```js
const getStory = async (id) => {
  try {
    const story = await axios.get(`${BASE_API_URL}/item/${id}.json`);
    return story;
  } catch (error) {
    console.log('Error while getting a story.');
  }
};
```

But the Axios library gives an actual response only in the `story.data` property. So we can modify the code to just return the data property from the `getStory` function.

```js
const getStory = async (id) => {
  try {
    const story = await axios.get(`${BASE_API_URL}/item/${id}.json`);
    return story.data;
  } catch (error) {
    console.log('Error while getting a story.');
  }
};
```

We can further simplify it as shown below:

```js
const getStory = async (id) => {
  try {
    const { data } = await axios.get(`${BASE_API_URL}/item/${id}.json`);
    return data;
  } catch (error) {
    console.log('Error while getting a story.');
  }
};
```

Here, we're using destructuring to extract the `data` property of the response and return that from the function.

Also, add the `console.log` statement back inside the `.then` handler of `dataFetcher.js` file:

```js
useEffect(() => { 
  ...
  .then((stories) => {
     console.log('stories', stories);
 ...
});
```

Now, If you check the application, you will see the following screen:

![Direct data](https://gist.github.com/myogeshchavan97/aa75611665802aadfd3ba6bfeb0fe59b/raw/b78b42d29fce5ea6ac66212822b7031cad63f824/direct_data.png)

As you can see, now we're getting direct data inside each element of the array as opposed to the object seen previously.

But we also get an error saying `Cannot read property 'id' of undefined` inside the `ShowStories.js` file.

This is because we're using the array `map` method inside the `ShowStories.js` file like this:

```js
{stories.map(({ data: story }) => (
  story && <Story key={story.id} story={story} />
))}
```

Previously, each array element was an object containing the `data` property so it was working fine as we were destructuring the `data` property and renaming it to `story`.

Now, we have the contents of the `data` object directly inside each array element so we need to change the above code to the below code:

```js
{stories.map((story) => (
  story && <Story key={story.id} story={story} />
))}
```

You can name the callback function variable to anything you like, I have named it `story` here.

Now, after making this change, If you check the application, you will see that the application is working fine as before without any issue. 

![Instant Loading](https://gist.github.com/myogeshchavan97/aa75611665802aadfd3ba6bfeb0fe59b/raw/d2fc75049a70d083f68598dd2e92b566281d4d59/instant_loading.gif)

That's it about this article. I hope you learned something new today.

## Closing points

You can find the complete GitHub source code for this article, in [this repository](https://github.com/myogeshchavan97/hackernews-clone-react-app-improved), and a live demo [here](https://hackernews-clone-react-app-improved.netlify.app/top).

Want to learn all ES6+ features in detail including let and const, promises, various promise methods, array and object destructuring, arrow functions, async/await, import and export and a whole lot more?

Check out my [Mastering Modern JavaScript](https://modernjavascript.yogeshchavan.dev/?coupon=LA1HR55) book. This book covers all the pre-requisites for learning React and helps you to become better at JavaScript and React.

Also, check out my free [Introduction to React Router](https://yogeshchavan1.podia.com/react-router-introduction) course to learn React Router from scratch.

Want to stay up to date with regular content regarding JavaScript, React, Node.js? [Follow me on LinkedIn](https://www.linkedin.com/in/yogesh-chavan97/).

[<img src="https://cdn.buymeacoffee.com/buttons/default-yellow.png" >](https://www.buymeacoffee.com/myogeshchavan97)