Avatar of Anthony Oyathelemhi

Anthony Oyathelemhi

Writing reusable custom hooks for data fetching in React applications

May 11, 2020 - 5 min read

Introduced in late 2018 (React 16.8), Hooks are a way to "reuse stateful logic between components". In summary, Hooks provide a way to take advantage of all the benefits of React without having to write classes.

Generally, they solve 3 common problems when writing class-based React components:

  • Reusing stateful logic between components
  • Separating unrelated logic in lifecycle methods
  • The overhead of learning how classes work in JavaScript

Data fetching in React

In a typical data-driven client-based React application, all external data is fetched after the component mounts. The way to do this with the hooks-based API is to use the built-in useEffect hook. We then update state with the response from the network request

import { useEffect, useState } from 'react';

function BlogPosts() {
  const [blogPosts, setBlogPosts] = useState([]);

  useEffect(() => {
    async function fetchBlogPosts() {
      const response = await fetch('/path/to/remote/data/source');
      setBlogPosts(response.json());
    }

    fetchBlogPosts();
  }, []);

  return (
    <ul>
      {blogPosts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

And just like that, we have created a component that renders a list of blog posts. But what if we needed the same list of blog posts in another component? We are going to look at several ways to do this

Replicate the same logic in the other component

This is the obvious solution. Just do the same thing wherever that data is needed and call it a day. Clearly this is not an ideal solution because you generally want to avoid repeating yourself when writing code, especially when it comes to data fetching. We want to avoid doing this because:

  • Making 2 network requests for the same data is inefficient and expensive
  • The data may change between the 2 network requests and potentially be out of sync
  • It reduces maintainability and can easily introduce subtle hard-to-find bugs

There needs to be a better way to do this

Move data fetching up the tree

The goal here is to have a single source of truth for the resource, so that all subscribers will always be in sync and we only have to make one network request. So we move the data fetching logic to a component that is a parent to all components that need the blog list data

function ParentComponent() {
  const [blogPosts, setBlogPosts] = useState([]);

  useEffect(() => {
    async function fetchBlogPosts() {
      const response = await fetch('/path/to/remote/data/source');
      setBlogPosts(response.json());
    }

    fetchBlogPosts();
  }, []);

  return (
    <div>
      <BlogPosts blogPosts={blogPosts} />
      <Tags blogPosts={blogPosts} />
    </div>
  );
}

function BlogPosts({ blogPosts }) {
  return (
    <ul>
      {blogPosts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

function Tags({ blogPosts }) {
  const tags = new Set(blogPosts.map(post => post.tag));

  return (
    <ul>
      {tags.map(tag => (
        <li key={tag}>{tag}</li>
      ))}
    </ul>
  );
}

We have solved a number of issues with this simple refactor. The network request is made once, data is guaranteed to be consistent and it is much easier to maintain. This is fine for many use cases as long as the subscribers to blogPosts are direct descendants (or close) of ParentComponent, otherwise we find ourselves with a new problems, prop drilling and having to always refactor our code to move blogPosts up the tree where it is available to all subscribers.

What if we could extract the data fetching to a helper function that we can call from anywhere in our application and not worry about refactoring, prop drilling and out-of-sync data?

Custom Hooks

With the introduction of Hooks, the React team also added the ability to build your own hooks by wrapping any of the built-in hooks in a function that starts with "use", e.g. useSomething. See rules of hooks

Let's create our useBlogPosts custom hook

function useBlogPosts() {
  const [blogPosts, setBlogPosts] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchBlogPosts();
  }, []);

  async function fetchBlogPosts() {
    const response = await fetch('/path/to/remote/data/source');
    setBlogPosts(response.json());
    setLoading(false);
  }

  return { blogPosts, loading };
}

Now we can use this hook anywhere we need the list of blogs

function BlogPosts() {
  const { blogPosts, loading } = useBlogPosts();

  if (loading) return <p>Loading...</p>;

  return (
    <ul>
      {blogPosts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

It is important to note that components using this hook DO NOT share the same state. The network request will be made everytime a component that uses it mounts.

But we want ALL components to have a synchronized state. We need to somehow make all of them reference the same blogPosts array.

uswSWR

useSWR is a custom remote data fetching hook by Vercel. This library solves the "out-of-sync" problem we would face if we used our previous custom hook. It caches the data returned from the network requests, which will instantly be available when another component requests for the same data

Let's add this to our custom hook

function useBlogPosts() {
  /**
   * You can pass a second argument to uswSWR, which it will use a the fetcher
   * In this case, it uses the default browser fetch api
   */
  const { blogPosts, error } = uswSWR('/path/to/remote/data/source');

  return {
    blogPosts,
    error,
    loading: !blogPosts && !error,
  };
}

Now all subscribers to this hook will always have an up-to-date version of the blog posts list

Here is a codesandbox example to demonstrate how this works

I'm happy to answer any questions you might have about this blog post, or anything else. Shoot me a DM on Twitter.