29 November 2018

cover

React Hooks Tutorial on Developing a Custom Hook for Data Fetching

Hooks are coming in React 16.7.
Share on:

Introduction

React Hooks are a new feature coming in React 16.7. It allows us to write stateful function components that were impossible before without class components. The official docs are must-read, so check them out if you haven’t.

https://reactjs.org/docs/hooks-intro.html

Besides stateful function components, this new feature allow us to build a custom hook to share logic between components. This has been possible with High-order Components (HoCs), and although there’s technically no difference what hooks can achieve, hooks simplify it a lot and reduce so called “wrapper hell”. This simplicity encourages building custom hooks, and this trend reminds me of npm’s early days.

This article explains how to write a custom hook and shows how it can share logic even if it’s a few lines of code. We take a simple data fetching (Fetch API) as an example.

The goal

Given the readers already learned the basis of React Hooks, we’d start with the goal example how a custom hook we develop would be used.

const MyComponent = () => {
  const { error, loading, data } = useFetch('http://...');
  if (error) return <Err error={error} />;
  if (loading) return <Loading />;
  return (
    <DataView data={data} />
  );
};

Here, useFetch is the custom hook. Isn’t it intuitive? MyComponent is a “stateful” function component whereas DataView is can be a stateless function component which should be more reusable.

Writing a custom hook

Let’s see how we write a custom hook to realize the above goal. A custom hook is just a function that uses some other hooks. We define a function first.

const useFetch = (url) => {
  // ...
};

This hook returns three values, so we define three state values. Note that we don’t need to combine them in one state object.

const useFetch = (url) => {
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(true);
  const [data, setData] = useState(null);
  // ...
  return { error, loading, data };
};

Now, the main part is defined in a useEffect hook. Notice, we pass url in the input array.

const useFetch = (url) => {
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(true);
  const [data, setData] = useState(null);
  useEffect(() => {
    (async () => {
      setLoading(true);
      const response = await fetch(url);
      const data = await response.json();
      setData(data);
      setLoading(false);
    })();
  }, [url]);
  return { error, loading, data };
};

We haven’t implemented error handling. Let’s add it.

const useFetch = (url) => {
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(true);
  const [data, setData] = useState(null);
  useEffect(() => {
    (async () => {
      setLoading(true);
      try {
        const response = await fetch(url);
        if (response.ok) {
          const data = await response.json();
          setData(data);
        } else {
          setError(new Error(response.statusText));
        }
      } catch (e) {
        setError(e);
      }
      setLoading(false);
    })();
  }, [url]);
  return { error, loading, data };
};

This is the basic custom hook to achieve our goal. It’s reusable in several components. You can check out the working example in this codesandbox.

https://codesandbox.io/s/github/dai-shi/react-hooks-fetch/tree/master/examples/01_minimal

Extending the custom hook

Our hook is still basic, and we might want to support POST method or non-JSON data. There could be various ways to extend this hook. Let’s try a simple way to keep Fetch API as much as possible.

const defaultOpts = {};
const useFetch = (input, opts = defaultOpts) => {
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(true);
  const [data, setData] = useState(null);
  const {
    readBody = body => body.json(),
    ...init
  } = opts;
  useEffect(() => {
    (async () => {
      setLoading(true);
      try {
        const response = await fetch(input, init);
        if (response.ok) {
          const body = await readBody(response);
          setData(body);
        } else {
          setError(new Error(response.statusText));
        }
      } catch (e) {
        setError(e);
      }
      setLoading(false);
    })();
  }, [input, opts]);
  return { error, loading, data };
};

One of the difficulties of this extended custom hook is that opts is passed in the input array of useEffect. Unless users carefully understand how it works, it may cause calling fetches infinitely.

The following is one example how to use this custom hook.

const PostRemoteData = () => {
  const opts = useMemo(() => ({
    method: 'POST',
    body: JSON.stringify({
      title: 'foo',
      body: 'bar',
      userId: 1,
    }),
    readBody: body => body.text(),
  }), []);
  const { error, loading, data } = useFetch('http://...', opts);
  if (error) return <Err error={error} />;
  if (loading) return <Loading />;
  return (
    <span>Result: {data}</span>
  );
};

The library

Although this is just a tiny custom hook, I made it as a library and publish it in npmjs.com.

https://github.com/dai-shi/react-hooks-fetch

Final notes

Actually, I’m not quite sure if Fetch API is an appropriate topic to explain building a custom hook. People might prefer data fetching layers isolated from view layers. Nevertheless, I hope this article explains how to develop a custom hook, and helps noticing its reusability.

comments powered by Disqus