Creating a custom hook!

Bartłomiej Szajewski

In this article, you will see, how to use that knowledge to convert class component, which gets data from JSONPlaceholder into functional component based on hooks. Then you will learn, how to separate logic from component into a custom hook!

Class component

As an example, I’m going to show a blog details page which contains information about current post (title and text) and navigation buttons that allow users to navigate to other posts.

const DEFAULT_POST_ID = 1;
const POSTS_URL = "https://jsonplaceholder.typicode.com/posts/";

class Post extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      postID: DEFAULT_POST_ID,
      data: {}
    };
  }

  getData() {
    fetch(POSTS_URL + this.state.postID)
      .then(res => res.json())
      .then(data => {
        this.setState({ data });
      });
  }

  componentDidMount() {
    this.getData();
  }

  componentDidUpdate(prevProps, prevState) {
    if (prevState.postID !== this.state.postID) {
      this.getData();
    }
  }

  setPostId(value) {
    this.setState({
      postID: this.state.postID + value || DEFAULT_POST_ID
    });
  }

  render() {
    const {
      data: { title, body }
    } = this.state;

    if (!title && !body) {
      return <div>Is loading...</div>;
    }
    return (
      <div>
        <button onclick="{()" ==""> this.setPostId(-1)}>Previous post</button>
        <button onclick="{()" ==""> this.setPostId(1)}>Next post</button>
        <h2>{title}</h2>
        <p>{body}</p>
      </div>
    );
  }
}

I created a React class component which keeps current post id (we want to navigate between posts) and fetched data (we want to display the post) in the state. After mounting, component asynchronously gets data from API using getData method, which performs fetching from proper URL. It’s constructed from two parts – POSTSURL_ and current post id (by default it is always 1, so if you refresh the page you will always fetch the information about first post). A loading indicator has been added to prevent the user from seeing an empty page until the API response is available. The next goal was to allow the users to navigate between the posts. To achieve this, I created two buttons Previous post and Next post. Each of them calls the setPostId method with proper value. In effect, current post ID is updated in the component state. API respects only positive numbers, so I added a simple check that ensures the new ID will be always greater than 0. The last thing was to fetch new data after the id update. I decided to do that in componentDidUpdate method. Why? You will see later 🙂 The getData method inside of componentDidUpdate function does a check to ensure that ID has changed. Otherwise, the same data would be fetched, even though it’s already in the state. And voilà! Component works as expected!

Functional component with hooks

The next step was to convert a previously created class component into functional one. I simply created a new one below and started to rewrite it with the hooks!

const DEFAULT_POST_ID = 1;
const POSTS_URL = "https://jsonplaceholder.typicode.com/posts/";

const Post = () => {
  const [postID, setPostID] = useState(DEFAULT_POST_ID);
  const [{ title, body }, setData] = useState({});

  if (!title && !body) {
    return <div>Is loading...</div>;
  }
  return (
    <div>
      <button onclick="{()" ==""> setPostID(postID - 1 || DEFAULT_POST_ID)}>
        Previous post
      </button>
      <button onclick="{()" ==""> setPostID(postID + 1)}>Next post</button>
      <h2>{title}</h2>
      <p>{body}</p>
    </div>
  );
};

How simple was that? I just copied render method into the function, wrote two lines of code with useState hooks. The only missing part was the data fetching. Brilliant! What’s more, it took me less than 3 minutes!

In the next step, I’ve added useEffect which, as you probably know, performs after each render. That’s just what we want to achieve! We’d like to have the data fetched after the first render (the same as in componentDidMount), as well as in the other renders too (similar to componentDidUpdate). I’ve achieved that by putting fetch method into useEffect hook. But it seems that something’s missing… the additional check for postID changes! I could use the second, optional parameter, which is an array of dependencies. The solution is to put postID wrapped into an array as useEffect’s second parameter. That’s all! Everything still works the same as in class component.

const DEFAULT_POST_ID = 1;
const POSTS_URL = "https://jsonplaceholder.typicode.com/posts/";

const Post = () => {
  const [postID, setPostID] = useState(DEFAULT_POST_ID);
  const [{ title, body }, setData] = useState({});

  useEffect(() => {
    fetch(POSTS_URL + postID)
      .then(res => res.json())
      .then(data => setData(data));
  }, [postID]);

  if (!title && !body) {
    return <div>Is loading...</div>;
  }
  return (
    <div>
      <button onclick="{()" ==""> setPostID(postID - 1 || DEFAULT_POST_ID)}>
        Previous post
      </button>
      <button onclick="{()" ==""> setPostID(postID + 1)}>Next post</button>
      <h2>{title}</h2>
      <p>{body}</p>
    </div>
  );
};

Maybe you have already noticed that in the class component we were using getData method which was called twice – in componentDidMount and componentDidUpdate functions. And what about DRY? With the help of hooks, we reduce usage of fetch to one, so it can be written inline. How cool is that?

Functional component with custom hook

Let’s just copy-paste the part which gets the data into another component… that doesn’t look good, don’t you think? That’s were hooks comes into action. They’ve been created to solve the problem of reusable, stateful function, with a simple interface to create your own hooks! Let’s try to do it that way!

Create a new file useFetch.js and create an empty function inside:

const useFetch = () => {};

We have previously implemented fetch, so let’s cut it and paste inside a function:

const useFetch = () => {
  useEffect(() => {
    fetch(POSTS_URL + postID)
      .then(res => res.json())
      .then(data => setData(data));
  }, [postID]);
};

We still don’t have the setData method, so let’s cut another part of the code and paste it on the very beginning of function body:

const useFetch = () => {
  const [{ title, body }, setData] = useState({});
  useEffect(() => {
    fetch(POSTS_URL + postID)
      .then(res => res.json())
      .then(data => setData(data));
  }, [postID]);
};

Great! Before proceeding, try to answer these questions:

  1. what arguments should useFetch take?
  2. what should the hook return?
  3. when the effect should be updated?

It’s quite possible that we’d want like to pass URL to our useFetch hook. We’d also like to get the fetched data back. The hook should be updated whenever URL changes. Pretty simple, huh? So let’s go back to the code!

const useFetch = url => {
  // 1
  const [{ title, body }, setData] = useState({});

  useEffect(() => {
    fetch(url) // modified - dynamic url instead of partially static
      .then(res => res.json())
      .then(data => setData(data));
  }, [url]); // 3

  return { title, body }; // 2
};

You created your first own hook! Congratulations! Now you can use it in everywhere in your project!

const DEFAULT_POST_ID = 1;
const POSTS_URL = "https://jsonplaceholder.typicode.com/posts/";

const Post = () => {
  const [postID, setPostID] = useState(DEFAULT_POST_ID);
  const { title, body } = useFetch(POSTS_URL + postID);

  if (!title && !body) {
    return <div>Is loading...</div>;
  }
  return (
    <div>
      <button onclick="{()" ==""> setPostID(postID - 1 || DEFAULT_POST_ID)}>
        Previous post
      </button>
      <button onclick="{()" ==""> setPostID(postID + 1)}>Next post</button>
      <h2>{title}</h2>
      <p>{body}</p>
    </div>
  );
};

Summary

Hopefully, you’ve enjoyed that tutorial and I helped you with understanding, what are the benefits of hooks, how to quickly convert React class components into functional ones and how to create own, reusable hooks.

Poznaj mageek of j‑labs i daj się zadziwić, jak może wyglądać praca z j‑People!

Skontaktuj się z nami