BBV logo

Data-Fetching using React Hooks

Jonatan Salas

Jul 06, 2019 - 6 min read

Data fetching is a common task, that we normally perform when building a component. In this post we’ll see how to turn a class based component into a functional one, which relies on hooks for fetching data.

The use case

Let’s suppose that we’re building a PostList component, which retrieves data from an API, and manages the following states:

  • Empty state
  • Error state
  • Loading state

The code for our PostList component, would be the following:

// PostList.js
import 'whatwg-fetch';
import 'abortcontroller-polyfill';

import React from 'react';

import Post from '@components/Post';
import Container from '@components/Container';

export default class PostList extends React.Component {
  static displayName = 'PostList';

  static defaultProps = {
    url: 'https://jsonplaceholder.typicode.com/posts',
  };

  // Create an AbortController instance
  abortController = new AbortController();

  state = {
    isLoading: true,
    posts: [],
    error: null,
  };

  componentDidMount() {
    this.makeFetchHappen(this.props.url);
  }

  componentDidUpdate(prevProps) {
    if (this.props.url !== prevProps.url) {
      this.makeFetchHappen(this.props.url);
    }
  }

  componentWillUnmount() {
    if (this.abortController) {
      // Cancel request when component unmounts
      this.abortController.abort();
      this.abortController = null;
    }
  }

  render() {
    const { isLoading, posts, error } = this.state;

    return (
      <Container
        isLoading={isLoading}
        isError={!isLoading && error}
        isEmpty={!isLoading && posts && posts.length === 0}
      >
        {posts.map((post, idx) => (
          <Post key={`Post.${idx}`} {...post} />
        ))}
      </Container>
    );
  }

  makeFetchHappen = async url => {
    try {
      if (!url) {
        throw new Error(`'url' is required for data-fetching`);
      }

      const { signal } = this.abortController;

      // Setup cancellation by passing the signal inside AbortController instance
      const response = await fetch(url, { signal });
      const posts = await response.json();

      this.setState({ posts });
    } catch (error) {
      this.setState({ error });
    } finally {
      this.setState({ isLoading: false });
    }
  };
}

As you can see, the component initializes its own state for managing the different phases for rendering. The data-fetching requirement is meeting during componentDidMount, and in the case of url changing when passing as prop to PostList, we also define the componentDidUpdate callback, which will retry data-fetching.


The problem

When working in real-apps you don’t often rely in one data-fetching call, you’ll have multiple ones, also, multiple components will be fetching-data to meet its own requirements.

Our current solution is attached to only one specific use case. If we want to translate this to another use cases, like getting a post by its own id, we’ll have to make another component and replicate some part of the data-fetching logic that could be or needs to be in another part, not in the component itself.


The partial solution

When getting into this problem, the most common solution is to make a generic fetch method, and create a service, like in this example:

// services.js
import 'whatwg-fetch';
import 'abortcontroller-polyfill';

const doFetch = async (url, options) => {
    try {
      if (!url) {
        throw new Error(`'url' is required for data-fetching`);
      }

      const response = await fetch(url, options);
      const data = await response.json();

      return [false, data, null];
    } catch (error) {
      return [false, null, error];
    }
  };
}

export const getPosts = ctx => doFetch(`https://jsonplaceholder.typicode.com/posts`, ctx);

export const getPostById = ctx => doFetch(`https://jsonplaceholder.typicode.com/posts/${ctx.id}`, ctx);

And then, you would rewrite your PostList component like the following:

import React from 'react';

import Post from '@components/Post';
import Container from '@components/Container';

import { getPosts } from '@services';

export default class PostList extends React.Component {
  static displayName = 'PostList';

  // Create an AbortController instance
  abortController = new AbortController();

  state = {
    isLoading: true,
    posts: [],
    error: null,
  };

  componentDidMount() {
    this.makeFetchHappen({ signal: this.abortController.signal });
  }

  componentWillUnmount() {
    if (this.abortController) {
      // Cancel request when component unmounts
      this.abortController.abort();
      this.abortController = null;
    }
  }

  render() {
    const { isLoading, posts, error } = this.state;

    return (
      <Container
        isLoading={isLoading}
        isError={!isLoading && error}
        isEmpty={!isLoading && posts && posts.length === 0}
      >
        {posts.map((post, idx) => (
          <Post key={`Post.${idx}`} {...post} />
        ))}
      </Container>
    );
  }

  makeFetchHappen = async ctx => {
    const [isLoading, posts, error] = await getPosts(ctx);

    this.setState({
      isLoading,
      posts,
      error,
    });
  };
}

But why a Partial Solution?

As you can see above, the developer that works with this code will need to think about things related to data-fetching and component state, like:

  • Implementing Request cancellation
  • Coupling to React lifecycle callbacks
  • Updating state based on the data coming from the service call

This is a partial solution because it will keep us thinking and working on things that we might error.


The final solution

A better solution to this has issue arrived with React v16.8. Using React Hooks for data-fetching would make you forgive about things like I mentioned above.

You can make a custom hook for data-fetching, like the following one:

// useFetch.js
import { useEffect, useRef, useState } from 'react';

const log = (...args) => console.warn(...args);

const useFetch = (url, options) => {
  const abortControllerRef = useRef(null);

  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(true);
  const [response, setResponse] = useState(null);

  useEffect(() => {
    abortControllerRef.current = new AbortController();
  });

  useEffect(() => {
    const { signal } = abortControllerRef.current;

    const makeFetchHappen = async () => {
      try {
        if (!url) {
          throw new Error(`'url' is required for fetching data`);
        }

        const response = await fetch(url, { ...options, signal });
        let data;

        try {
          data = await response.json();
        } catch (error) {
          log(`useFetch: can't parse JSON, trying parsing response as text`);
          data = await response.text();
        } finally {
          setResponse(data);
        }
      } catch (error) {
        log(`useFetch: ${error.message}`);
        setError(error);
      } finally {
        setLoading(false);
      }
    };

    makeFetchHappen();

    return () => abortControllerRef.current.abort();
  }, [url, options]);

  return [loading, response, error];
};

export default useFetch;

And then, you would convert your class based component to a function one:

// PostList.js
import 'whatwg-fetch';
import 'abortcontroller-polyfill';

import React from 'react';

import Post from '@components/Post';
import Container from '@components/Container';

import useFetch from '@hooks/useFetch';

const PostList = props => {
  const [isLoading, posts, error] = useFetch(
    'https://jsonplaceholder.typicode.com/posts'
  );

  return (
    <Container
      isLoading={isLoading}
      isError={!isLoading && error}
      isEmpty={!isLoading && posts && posts.length === 0}
    >
      {posts &&
        posts.map((post, idx) => <Post key={`Post.${idx}`} {...post} />)}
    </Container>
  );
};

PostList.displayName = 'PostList';

export default PostList;

As you can see, we’ve abstracted all the stuff in our custom useFetch hook. Our custom hook will be in charge of:

  • Managing different states for our data-requirements
  • Fetching data in componentDidMount/componentDidUpdate
  • Managing request-cancellation when our component using this hook unmounts

We could increase the complexity of this hook by adding support for:

  • Managing GraphQL queries
  • Caching results from an API for faster updates
  • Parsing different data-types when calling an API
  • Making fetching-data optional in React lifecycle via useEffect

But for the educational purposes of this post we won’t make those changes.

Well, that’s all, I hope you’ve enjoyed it. If you really do, just live a reply or reaction, I would like to hear feedback from you.

Jonatan Salas

Co-Founder and CTO at BlackBox Vision

Subscribe for latest updates

Sign Up for our newsletter and get notified when we publish new articles for free!

arrow back iconPrevious