DEV Community

Mohd Nisab Alam.
Mohd Nisab Alam.

Posted on

Create your own API fetching & caching mechanism in React

Image description

To fetch data from an API and display a response in a React application, you need to create a component that handles the API request. Inside the component, you can use the useState hook to initialize a state variable to hold the fetched data. Then, use the useEffect hook to fetch the data when the component mounts. Inside the useEffect hook, make an HTTP request to the API using methods like fetch or a library like Axios. Once you receive the response, update the state variable with the fetched data. Finally, render the response in the component's JSX, accessing the relevant data from the state.

Yup that is great but so much of code and not efficient, The main thing is you cant cache you API responses, So if your server is bit slow and you don’t want to keep making request for same query then caching at client side will be the an option for you. Caching API responses can improve the performance and efficiency of your application by reducing unnecessary network requests.

There are plenty of libraries which gives you this out of box, such as TanStackQuery and few more.

Understanding how things work instead of relying solely on libraries can have several advantages:

  • Flexibility and customization: When you have a deep understanding of how things work, you have more control and flexibility over your code. You can customize and adapt solutions to meet specific requirements without being limited by the functionalities provided by a library.

  • Troubleshooting and debugging: When you encounter issues or bugs in your code, having a deeper understanding of the underlying mechanisms can help you troubleshoot and debug more effectively. You can identify and fix problems by examining the code and the logic behind it.

  • Efficiency and performance: Libraries often come with additional overhead, such as extra dependencies, size, or processing time. By understanding how things work, you can optimize your code for efficiency and performance, potentially avoiding unnecessary dependencies or streamlining processes.

  • Learning and growth: Exploring how things work on a fundamental level allows you to expand your knowledge and skills. It enhances your ability to grasp new concepts, solve complex problems, and adapt to changing technologies and frameworks.

However, it’s important to strike a balance. Libraries and frameworks can provide convenience, speed up development, and handle complex tasks more efficiently. They are often built by experts and undergo rigorous testing. Leveraging libraries can save time and effort, especially for common or well-established functionalities.

Ultimately, the choice between understanding how things work and relying on libraries depends on the specific context, project requirements, and personal preferences. A combination of both approaches can often yield the best results, leveraging libraries when appropriate and delving into the underlying concepts when necessary.

In order to understand what is caching lets understand first what is cache? In computing, a cache is a temporary storage location that stores frequently accessed data or instructions in order to expedite future requests for that data. The purpose of a cache is to improve system performance by reducing the time and resources required to retrieve data from the original source.

Okay now we understand that cache means a temporary data storage, so how do we store this cache, but more importantly when we store some data how will we be accessing the particular data you need in an efficient way. So you can store a cache with a key. So best way is an creating and object with keys and data as value, or a map data structure to perform the same.

So here starts the implementation of cache in react (not fetching just caching).

This cache should be available to all components, so lets keep this over a context and wrap over main component.

import { createContext, useContext, ReactNode } from "react";

type ContextType = {
  getCache: (key: string) => any;
  setCache: (key: string, value: any, ttl?: number) => void;
  clearCache: () => void;
  deleteCache: (key: string) => void;
};

type cacheBody = {
  expiry: Date;
  data: any;
};

const CacheContext = createContext<ContextType | null>(null);

export function useCache() {
  return useContext(CacheContext) as ContextType;
}

export default function CacheProvider({ children }: { children: ReactNode }) {
  const map = new Map<string, cacheBody>();

  function getCache(key: string) {
    const cacheValue = map.get(key);
    if (!cacheValue) return undefined;
    if (new Date().getTime() > cacheValue.expiry.getTime()) {
      map.delete(key);
      return undefined;
    }
    return cacheValue.data;
  }

  function setCache(key: string, value: any, ttl: number = 10) {
    var t = new Date();
    t.setSeconds(t.getSeconds() + ttl);
    map.set(key, {
      expiry: t,
      data: value
    });
  }

  function clearCache() {
    map.clear();
  }

  function deleteCache(key: string) {
    map.delete(key);
  }

  const contextValue = {
    getCache,
    setCache,
    clearCache,
    deleteCache
  };

  return (
    <CacheContext.Provider value={contextValue}>
      {children}
    </CacheContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

As now the context is created lets wrap our App by this CacheContext. We can see the cache context gives you four methods to set, delete, get & empty the cache, The set method takes a key string , a value of type any and a ttl(Time to live in seconds) which determines when this cache should be invalidated. The get method gives you cache if present under expiry time otherwise gives you undefined, The other two functions are to remove a cache or to empty cache respectively.

<CacheProvider>
    <App />
</CacheProvider>
Enter fullscreen mode Exit fullscreen mode

So, now our caching mechanism is ready, but as our aim is to cache the API responses, But now if we check if cache is present in every component while making request would not be a great idea. Instead we can create a custom hook which will do everything for us out of the box, We just need to provide API URL, key and few config.

import { useEffect, useState } from "react";
import { useCache } from "../contexts/Cache";
import axios, { AxiosRequestConfig, AxiosResponse, AxiosError } from "axios";

type CustomAxiosConfig = AxiosRequestConfig & {
  key: Array<unknown>;
  initialEnabled?: boolean;
  cache?: {
    enabled?: boolean;
    ttl?: number;
  };
  onSuccess?: (data: AxiosResponse) => void;
  onFailure?: (err: AxiosError) => void;
};

function keyify(key: CustomAxiosConfig["key"]) {
  return key.map((item) => JSON.stringify(item)).join("-");
}

export default function useFetch<T = any>({
  key,
  initialEnabled = true,
  cache,
  ...axiosConfig
}: CustomAxiosConfig) {
  const [loading, setLoading] = useState(false);
  const [data, setData] = useState<T | undefined>();
  const [error, setError] = useState<any>();
  const { getCache, setCache, deleteCache } = useCache();

  const refetch = (hard: boolean = false) => {
    setLoading(true);
    setError(undefined);
    const cacheKey = keyify(key);
    if (cache?.enabled && getCache(cacheKey) !== undefined && !hard) {
      setData(getCache(cacheKey));
      setLoading(false);
      setError(undefined);
      return;
    }
    axios(axiosConfig)
      .then((data) => {
        setData(data as T);
        if (cache?.enabled) setCache(cacheKey, data, cache.ttl);
      })
      .catch((err) => {
        setError(err);
      })
      .finally(() => {
        setLoading(false);
      });
  };

  function inValidate(invalidationKey: CustomAxiosConfig["key"]) {
    deleteCache(keyify(invalidationKey));
  }

  useEffect(() => {
    if (initialEnabled) refetch();
  }, []);

  return { loading, data, error, refetch, inValidate } as const;
}
Enter fullscreen mode Exit fullscreen mode

Here is an custom hook called useFetch which can be called in a component and it provides you all the benefits of state management. You don’t have to maintain multiple states such as loading, error and data in component. This is all returned by the hook itself. The hook also provides you methods to refetch and invalidate a query.

import useFetch from "./hooks/useFetch";

export default function App() {
  const { loading, error, data, refetch } = useFetch({
    url: "https://randomuser.me/api",
    method: "get",
    key: ["app", "get", "user", { name: "nisab" }],
    cache: {
      enabled: true,
      ttl: 10
    }
  });

  if (loading) {
    return <p>Loading...</p>;
  }
  if (error) {
    return <p>Something went wrong</p>;
  }
  return (
    <div className="App">
      {JSON.stringify(data, null, 4)}
      <br />
      <button onClick={() => refetch()}>get user</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here is an implementation of useFetch hook which demonstrates the usage of fetching data and caching it if needed. The useFetch hook takes a config shown above where caching is optional. The hook config parameter also takes onSuccess & onFailure for callbacks and you can provide all other axios config in the parameter itself.

If you want to look complete implementation of this cache mechanism in react and the output how this works. Then do check out this sandbox repo where the complete working repo is hooked up.

Code Sandbox

Top comments (0)