⬅️ Back to posts

Abstract Data Fetching In React

10 Nov 2020 · 3 min

When I work with large code bases, I often find that they lack a clear and unified strategy for data fetching. A pattern I have noticed when using Hooks is the following code snippet repeated every time data need to be loaded:

function useData() {
  const [data, setData] = useState({
    isLoading: false,
    data: undefined,
    hasError: false,
  });

  const fetchData = async () => {
    setData(prevState => ({
      ...prevState,
      isLoading: true,
      hasError: false,
    }));
    try {
      // getData provides the requested data
      const response = await getData();
      setData(prevState => ({
        ...prevState,
        data: response,
        isLoading: false,
      }));
    } catch (error) {
      setData(prevState => ({
        ...prevState,
        isLoading: false,
        hasError: true,
      }));
    }
  };

  return { data, fetchData };
}

The main difference between each repetition of this code is getData. For instance, when fetching posts or profile information it could be getPosts or getUserInfo.

Even though in many circumstances repeating code is completely acceptable and even encouraged, I believe that this case should be abstracted. The reason why is because when thinking about user experience, the same functionality is usually expected by users every time they fetch new data.

Data fetching is usually triggered through some sort of action, such as clicking a button or typing into a search box. After this, if the data are not received immediately, users expect to see some sort of visual feedback like a spinner. If there is a problem getting the data, they expect an error message.

So, it would make sense to have a Hook that would abstract all of this functionality. One way this can be achieved is with the following:

const [{ loading, data, hasError }, fetchData] = useRequest(request);

The amount of typing involved is greatly reduced and now useRequest clearly returns what users need: a loading state, the requested data, an error state and a function for fetching the data.

One way useRequest could be implemented is by taking the same structure as in useData but now passing request as an argument of the Hook:

function useRequest(request) {
  const [loading, setLoading] = useState(false);
  const [data, setData] = useState(undefined);
  const [hasError, setHasError] = useState(false);

  async function fetchData(...args) {
    try {
      setLoading(true);
      setHasError(false);
      // arbitrary args can be sent through fetchData to request
      const response = await request(...args);
      setData(response);
    } catch (error) {
      setHasError(true);
    } finally {
      setLoading(false);
    }
  }

  return [{ loading, data, hasError }, fetchData];
}

Although useRequest provides a more declarative and simple solution for data fetching, there are a few more questions that remain open:

Several open source libraries for remote data fetching tackle these points. For further information, I recommend React Query and SWR.