useAsync hook with cache
Tags: javascript, react, async, reacthooks
Published: 2022-08-18 05:09:00

It's good practice to show the user that the app is loading data. This is done by showing a loading indicator, and hiding the content until the data is ready. Most of us will be maintaining a state in the component that tracks whether the data is ready or not and this is repeated in every component that calls an API.

Consider the following example:

##### Todos component

```jsx
import React, { useState, useEffect } from "react";
const Todos = () => {
  const [todos, setTodos] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    const init = async () => {
      try {
        setLoading(true);
        const response = await fetch(
          "https://jsonplaceholder.typicode.com/todos"
        );
        const data = await response.json();
        setTodos(data);
      } catch (error) {
        setError(error);
      } finally {
        setLoading(false);
      }
    };
    init();
  }, []);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error</div>;
  return (
    <div>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
    </div>
  );
};
```

---

##### TODO details

```jsx
const Todo = ({ id }) => {
  const [todo, setTodo] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    const init = async () => {
      try {
        setLoading(true);
        const response = await fetch(
          `https://jsonplaceholder.typicode.com/todos/${id}`
        );
        const data = await response.json();
        setTodo(data);
      } catch (error) {
        setError(error);
      } finally {
        setLoading(false);
      }
    };
    init();
  }, [id]);

  if (loading) return <div>Loading 2...</div>;
  if (error) return <div>Error 2</div>;
  return (
    <div>
      <h1>{todo.title}</h1>
    </div>
  );
};
```

As we can see, There are three main things that are happening in the code:

1. The first thing is that we are showing a loading indicator while the fetch is happening.
2. The second thing is that we are handling the error if there is one.
3. The third thing is that we are setting the todo state to the data that we got back from the API.

###### Note: Data fetching logic is the same in both components. We can create a custom hook that will be used to handle all asynchronous data fetching and updating the state.

---

### Custom hook(useAsync)

React hooks are a set of functions that can be used to create a component that is more flexible than the traditional component lifecycle.

We can create a custom hook that will be used to handle all asynchronous data fetching and updating the state.

##### useAsync hook

```jsx
import React, { useState, useEffect } from "react";

const useAsync = (defaultData) => {
  const [data, setData] = useState({
    data: defaultData ?? null,
    error: null,
    loading: false,
  });

  const run = async (asyncFn) => {
    try {
      setData({ data: null, error: null, loading: true });
      const response = await asyncFn();
      const result = { data: response, error: null, loading: false };
      setData(result);
      return result;
    } catch (error) {
      const result = { data: null, error, loading: false };
      setData(result);
      return result;
    }
  };

  return {
    ...data,
    run,
  };
};
```

---

##### Todos component

```jsx
import React, { useState, useEffect } from "react";
import { useAsync } from "./hooks";
const Todos = () => {
  const { data, loading, error, run } = useAsync([]);

  useEffect(() => {
    run(() => fetch("https://jsonplaceholder.typicode.com/todos").then((res) => res.json()));
  }, []);

  // Same as above
  return ...
};
```

---

##### TODO details

```jsx
import React, { useState, useEffect } from "react";
import { useAsync } from "./hooks";
const Todo = ({ id }) => {
  const { data, loading, error, run } = useAsync(null);

  useEffect(() => {
    run(() => fetch(`https://jsonplaceholder.typicode.com/todos/${id}`).then((res) => res.json()));
  }, [id]);

  // Same as above
  return ...
};
```

##### NOTE:

We have reduced the amount of code we have to write by using the custom hook. It's also easier to read and maintain the code.

##### Let's add more functionality to our custom hook

1. Add caching to the custom hook to prevent API calls if data is already present in the state.

```jsx
import { useState, useCallback } from "react";

const cache = new Map();
const defaultOptions = {
  cacheKey: "",
  refetch: false,
};

export const useAsync = (defaultData?: any) => {
  const [data, setData] = useState({
    data: defaultData ?? null,
    error: null,
    loading: false,
  });

  const run = useCallback(async (asyncFn, options = {}) => {
    try {
      // Merge the default options with the options passed in
      const { cacheKey, refetch } = { ...defaultOptions, ...options };

      const result = { data: null, error: null, loading: false };

      // If we have a cache key and not requesting a new data, then return the cached data
      if (!refetch && cacheKey && cache.has(cacheKey)) {
        const res = cache.get(cacheKey);
        result.data = res;
      } else {
        setData({ ...result, loading: true });
        const res = await asyncFn();
        result.data = res;
        cacheKey && cache.set(cacheKey, res);
      }
      setData(result);
      return result;
    } catch (error) {
      const result = { data: null, error: error, loading: false };
      setData(result);
      return result;
    }
  }, []);

  return {
    ...data,
    run,
  };
};
```

---

##### TODO details

```jsx
import React, { useState, useEffect } from "react";
import { useAsync } from "./hooks";
const Todo = ({ id }) => {
  const { data, loading, error, run } = useAsync(null);

  useEffect(() => {
    run(() => fetch(`https://jsonplaceholder.typicode.com/todos/${id}`)
        .then((res) => res.json()),
        {cacheKey: `todo-${id}`});
  }, [id]);

  // Same as above
  return ...
};
```

##### Options:

1. cacheKey: The key that we will use to store the data in the cache.
2. refetch: If we want to refetch the data from the API. This is useful when we want to refresh the data in the cache.

**NOTE:** Cache is available globally, so we can use it in other components. If we use useAsync in multiple components with the same cacheKey, then cache data will be shared across all the components. This is useful when we want to avoid unnecessary API calls if the data is already present in the cache.

###### React Query and SWR are two popular libraries that can be used to handle all asynchronous data fetching.

#### Live example, [here](https://stackblitz.com/edit/useasync-hook-with-cache-rahul-sharma-devsmitraa)

---

Thank you for reading 😊

Got any questions or additional? please leave a comment.

---

##### Must Read If you haven't

###### [3 steps to create custom state management library with React and Context API](https://dev.to/devsmitra/3-steps-to-create-custom-state-management-library-with-react-hooks-and-context-api-1bfh)

###### [How to cancel Javascript API request with AbortController](https://dev.to/devsmitra/how-to-cancel-javascript-api-request-with-abortcontroller-36cm)

###### [Getting started with SolidJs – A Beginner's Guide](https://dev.to/devsmitra/getting-started-with-solidjs-a-beginners-guide-5af4)

---

###### More content at [Dev.to](https://dev.to/devsmitra "Rahul Sharma(DevsMitra)").

###### Catch me on [YouTube](https://www.youtube.com/channel/UCQkvNcHN5vg3jEoeOb9Tdrw?sub_confirmation=1 "Rahul Sharma(DevsMitra)"), [Github](https://github.com/devsmitra "Rahul Sharma(DevsMitra)"), [Twitter](https://twitter.com/devsmitraa "Rahul Sharma(DevsMitra)"), [LinkedIn](https://www.linkedin.com/in/rahul-sharma-18b8b374 "Rahul Sharma(DevsMitra)"), [Medium](https://devsmitra.medium.com "Rahul Sharma(DevsMitra)"), [Stackblitz](https://stackblitz.com/@devsmitra "Rahul Sharma(DevsMitra)"), [Hashnode](https://devsmitra.hashnode.dev "Rahul Sharma(DevsMitra)"), [HackerNoon] (https://hackernoon.com/u/devsmitra "Rahul Sharma(DevsMitra)"), and [Blogspot](https://devsmitra.blogspot.com "Rahul Sharma(DevsMitra)").