DEV Community

SavagePixie
SavagePixie

Posted on • Edited on

Fetching data with React Hooks

React Hooks are a relatively new feature of React that allows us to keep the state of our app in a function component, rather than having to create a class component. According to React's documentation on Hooks, the appropriate way to handle data fetching seems to be using the useEffect hook. So I'll try doing just that.

For the purposes of this post, I am going to imagine that I want to make an app that fetches a random book from an API, shows it as a book recommendation and allows the user to get a new book recommendation.

Writing a useEffect hook

Writing a useEffect is relatively simple. We just need to create a function component and add some state to it.

const BookApp = () => {
  const [ book, setBook ] = useState({})

  useEffect(() =>
    fetch("BookApiUrl")
      .then(res => res.json())
      .then(setBook)
  )

  return(
    <div>
      <img src={book.cover} alt={`${book.title}'s cover page`} />
      <p className="title">{book.title}</p>
      <p className="author">{book.author}</p>
    </div>
  )
}

Now, don't try this at home. If you do, you'll set an infinite loop. But don't worry, we'll fix that in a bit. This useEffect will execute when the component has finished mounting and every time something in the state changes. That's why we get an infinite loop. It fetches data from a fictional API, then updates the state, then the state change triggers another call to the useEffect and so on.

Other than that, there isn't much interesting going on. A normal fetch fetches data from an API. Then the data gets jsonified. Then we use the hook setBook to update the state.

Stopping the infinite loop

The next thing to do is fixing that loop. useEffect takes two arguments. The first one is the function that it executes. The second one is an array of values. If these values haven't changed between re-renders, React will skip the effect.

const BookApp = () => {
  const [ book, setBook ] = useState({})
  const [ count, setCount ] = useState(0)

  useEffect(() =>
    fetch("BookApiUrl")
      .then(res => res.json())
      .then(setBook),
  [count])

  return(
    <div>
      <img src={book.cover} alt={`${book.title}'s cover page`} />
      <p className="title">{book.title}</p>
      <p className="author">{book.author}</p>
      <button onClick={() => setCount(count + 1)}>Get another book</button>
    </div>
  )
}

This is definitely not a very elegant solution, but it is the first one I came with. I'll do something about it later. Now our useEffect will only execute if count changes between re-renderings. Other state changes will not trigger another data fetching. Since the state only changes when the user clicks on the button, that's the only thing that will trigger the effect (well, that and the component mounting).

Tip if you want to mimic the behaviour of componentDidMount, you can simply add an empty array as the second argument. That will keep useEffect from executing on any state change.
useEffect(() => { do stuff }, [])

Making it more elegant

Having a state variable whose only purpose is to change when a button is clicked didn't strike me as a particularly good solution. As it stands now, there's another problem: the API might give us the same book twice in a row. Ideally we'd want to avoid that. So let's put both things together to make the code fancier.

Instead of having a counter that counts how many times the button has been clicked, I will store the previous book in the app's state and check that I don't get the same when I fetch the data.

const BookApp = () => {
  const [ book, setBook ] = useState({})
  const [ lastBook, setLastBook ] = useState("")

  useEffect(() => {
    const fetchBook = () =>
      fetch("BookApiUrl")
        .then(res => res.json())
        .then(data => {
          if (data.title == lastBook) return fetchBook()
          else setBook(data)
        })
    fetchBook()
  }, [lastBook])

  return(
    <div>
      <img src={book.cover} alt={`${book.title}'s cover page`} />
      <p className="title">{book.title}</p>
      <p className="author">{book.author}</p>
      <button onClick={() => setLastBook(book.title)}>Get another book</button>
    </div>
  )
}

Now the state that tells the effect hook when to execute also serves the purpose of keeping track of the last book retrieved and avoiding retrieving it again. One change to note is that, since now the function that fetches the data is potentially recursive, it needs a name so it can call itself.

Summary

If we need to fetch data from an API in a React app, we can place the fetching within an effect hook. We need to be careful not to set an infinite loop, though. We will need some way in the state to tell the hook when to execute. Other than that, it isn't much different than a normal fetch.

Top comments (6)

Collapse
 
mayuraitavadekar profile image
Mayur Aitavadekar • Edited

amazing post :) it really solved my problem.

here is what I wanted to do -
I was fetching the data from my backend method -

const [values, setValues] = useState({
    name: "",
    description: "",
    error: false,
    loading: false,
    success: false,
  });

const preload = (courseName) => {
    courseName = courseName.replace(/%20/g, " ");
    getCourseByName({ courseName }).then((data) => {
      if (data.error) {
        setValues({ ...values, error: data.error });
      } else {
        console.log("data retrieved : ", data);
        setValues({
          ...values,
          name: data.name,
          description: data.description,
          success: true,
        });
        console.log(values); // I was getting empty responses here
      }
    });
  };

  useEffect(() => {
    preload(match.params.courseName);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

after that I just added this to use effect method -

  useEffect(() => {
    preload(match.params.courseName);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [values.success]);

and I got my values printed right!

Summery : it will update the state of values only if value of success is changed :) but the problem here is that the values are getting printed 2 times.
Any thoughts?

Collapse
 
savagepixie profile image
SavagePixie

Just to make sure that I understand the problem. When you say that the values are being printed twice, you mean in the console, right? And, since you've got two console.logs, does that mean that it gets printed four times in total?

Collapse
 
mayuraitavadekar profile image
Mayur Aitavadekar • Edited

yes. console.log() print 4 times. Now below is detailed problem I had before doing comment on your post. see the useEffect.

first of all I wanted was to assign the fetched values to my state so that I can use it in my JSX of react component.
So I written following code :

const [values, setValues] = useState({
    name: "",
    description: "",
    error: false,
    loading: false,
    success: false,
  });

const preload = (courseName) => {
    courseName = courseName.replace(/%20/g, " ");
    getCourseByName({ courseName }).then((data) => {
      if (data.error) {
        setValues({ ...values, error: data.error });
      } else {
        console.log("data retrieved : ", data);
        setValues({
          ...values,
          name: data.name,
          description: data.description,
          success: true,
        });
        console.log(values); // use this values in react component
      }
    });
  };

  useEffect(() => {
    preload(match.params.courseName);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []); // here I kept the brackets empty first.

So I wanted to assign the fetched data to my JSX elements in component. So I thought why not to check if it is printing correctly. here is what happened:
console.log("data retrieved : ", data); prints exact retrieved data. but console.log(values); this prints all empty values. So I thought the setValues() didn't work out.

After that I started to find out why my retrieved data is not assigned to setValues(). So I saw your post and I entered [values.success] in useEffect().

useEffect(() => {
    preload(match.params.courseName);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [values.success]);

This correctly assigns fetched data into setValues and prints console.log(values); But problem here values gets printed two times. First with empty values and second with assigned values. This is mainly because useEffect method runs twice. first when DOM is mounted and second when value of success changed after fetching data from API. Hence I also didn't want this.

So I thought, lets keep this printing values aside. I did not print the
console.log(values) as I knew that it will be empty. I also didn't write console.log("data retrieved : ", data); as I knew that fetching works fine. What I did was, I kept the [ ] in useEffect() empty as before. I fetched data and assigned data values using setValues.

const [values, setValues] = useState({
    name: "",
    description: "",
    error: false,
    loading: false,
    success: false,
  });

const preload = (courseName) => {
    courseName = courseName.replace(/%20/g, " ");
    getCourseByName({ courseName }).then((data) => {
      if (data.error) {
        setValues({ ...values, error: data.error });
      } else {
        setValues({
          ...values,
          name: data.name,
          description: data.description,
          success: true,
        });
      }
    });
  };

  useEffect(() => {
    preload(match.params.courseName);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

After this, I directly used the values (values.name, value.description, value.loading, value.success) in my JSX and it worked fine and that was my end goal.

But I couldn't find solution to one question - why does console.log(values) prints empty values but in actuality the setValues works fine and assigns values and you can use those in your JSX.

PS: I know this comment has new level of verbosity ;)

Thread Thread
 
savagepixie profile image
SavagePixie

Thanks for such a detailed explanation! It is very helpful to understand what exactly you mean and to see what you've tried and why. Also, I'm glad you managed to get it to work as you wanted.

The reason why console.log() printed stuff twice is that useEffectwas running twice. Once when the page was first rendered (but before the data was fetched) and once on the re-render caused by retrieving the data. useEffect fires both on the first load of the page and on every state change (unless, obviously, you narrow it down in the dependency array). Since getCourseByName is an asynchronous operation, it will change the state after the page has loaded for the first time, thus triggering a re-render.

Collapse
 
dnafication profile image
Dina

Hi @savagepixie , thanks for the very nice post.

A question, is there still a chance of infinite loop if the recursive function fetchBook returns the same book every time? May be we need another stronger base case. Or it may be handled in the back end by ensuring that the book is never repeated.

Collapse
 
savagepixie profile image
SavagePixie • Edited

Thanks for your reply!

A question, is there still a chance of infinite loop if the recursive function fetchBook returns the same book every time?

Very good question. To tell the truth, I'm not completely sure. I think it wouldn't be an infinite loop, but it'd run twice (once after the component is mounted and once after the state is updated), which is still undesirable and should be addressed. I'll double-check when I get the chance, though.

EDIT: Okay, after a quick experiment, it seems to run into an infinite loop even if the data fetched is always the same.

Or it may be handled in the back end by ensuring that the book is never repeated.

We certainly can handle it in the backend. The principle would be the same, keep track of the last book sent and ensure it doesn't get repeated.