loading...

the correct way to fetch data with react hooks

nicomartin profile image Nico Martin Updated on ・3 min read

One thing first. I really like the flexibility of React. Going through the official React documentation I don't find a lot of must-use patterns or anti-patterns. The goal is clear: React is the framework, use it however you want. And in my opinion that's also one of the main advantages over more "opinionated" frameworks like VueJS or Angular.

The only problem is that this makes it quite easy to write messy code without even noticing. Let's take a very basic example. Let's assume you need to fetch some data:

// ./PostList.jsx
import React from 'react';

const PostList = () => {
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState('');
  const [data, setData] = React.useState([]);

  React.useEffect(() => {
    setLoading(true);
    fetch('https://api.mysite.com')
      .then((response) => response.json())
      .then((data) => {
        setLoading(false);
        setData(data);
      })
      .catch((e) => {
        setLoading(false);
        setError('fetch failed');
      });
  }, []);

  if (loading) {
    return <p>loading..</p>;
  }

  if (error !== '') {
    return <p>ERROR: {error}</p>;
  }

  return (
    <React.Fragment>
      <p>Data:</p>
      <ul>
        {data.map((element) => (
          <li>{element.title}</li>
        ))}
      </ul>
    </React.Fragment>
  );
};

At first sight this look ok. And to be honest that's pretty much how I made my api calls ever since I started with hooks.

The problem

But then there was this Tweet by Aleksej Dix, that made me thinking:


please ignore my stupid reply. I completely missunderstood his point at this time 🤦‍♂️

The problem seems to be pretty clear. There is no clear definition of what status the component has at any given time. The component status always depends on a combination of different "React-states". Maybe in this very simple example it's not too hard to "guess" the component states and handle them appropriately. But if you think about more complex examples in the wild you will quickly get into some troubles.

The second thing that bothered me was that the logic and the presentation are all mixed up in one component. It's not too bad but I just like to have a clear separation of those tasks. Also this makes it nearly umpossible to write meaningful unit tests.

The solution: custom hooks

After some discussions with friends and collegues I really wanted the try this one aproach: To create a custom hook that handles the fetch and the data so the actual component only needs to display the outcome. And here's my solution.

// ./useApi.jsx
import React from 'react';

export const apiStates = {
  LOADING: 'LOADING',
  SUCCESS: 'SUCCESS',
  ERROR: 'ERROR',
};

export const useApi = url => {
  const [data, setData] = React.useState({
    state: apiStates.LOADING,
    error: '',
    data: [],
  });

  const setPartData = (partialData) => setData({ ...data, ...partialData });

  React.useEffect(() => {
    setPartData({
      state: apiStates.LOADING,
    });
    fetch(url)
      .then((response) => response.json())
      .then((data) => {
        setPartData({
          state: apiStates.SUCCESS,
          data
        });
      })
      .catch(() => {
       setPartData({
          state: apiStates.ERROR,
          error: 'fetch failed'
        });
      });
  }, []);

  return data;
};
// ./PostList.jsx
import React from 'react';
import {apiStates, useApi} from './useApi.jsx'

const PostList = () => {
  const { state, error, data } = useApi('https://api.mysite.com');

  switch (state) {
    case apiStates.ERROR:
      return <p>ERROR: {error || 'General error'}</p>;
    case apiStates.SUCCESS:
      return (
        <React.Fragment>
          <p>Data:</p>
          <ul>
            {data.map((element) => (
              <li>{element.title}</li>
            ))}
          </ul>
        </React.Fragment>
      );
    default:
      return <p>loading..</p>;
  }
};

Yes, you could argue that the code is bigger now. But in the end we now have two completely separate functions, where each one has their single job. A hook that fetches the content and a component that displays the data. BTW, the hook could very well be used as some kind of high-order-hook that handles all API-requests of your application.

But more than this we can be sure that our hook will always return this one standardized object. A state (which has to be one of the defined apiStates), an error and a data-Array.
Even if we forget to reset the error after a second try it should not matter. If error is not empty we still know that the fetch was successfull because of the state we got from the hook.
My return object is of course a very simplified example. If you have more complex data it might makes sense to adjust those properties an make them more flexible (for example state and "generic" context). But I think it's enough to get the idea.

In my opinion this is so much more stable than the previous aproach. And last but not least it makes it easier to test both functions with unit tests.

Of course this is only one possible aproach to have propper state handling and separation of logic and view. So I'd really like to get your feedback in the comments!

Posted on by:

Discussion

markdown guide
 

Thank you for sharing your knowledge. I learn React and I'm happy to read a practical example, at last. But I have an error when I try to run the code:
'React Hook "React.useState" is called in function "datasource" which is neither a React function component or a custom React Hook function react-hooks/rules-of-hooks'
datasource is my useApi.
Could you help me, please?

 

Hi @shatvani
Where do you call useApi? Inside a Functional Component?

 

Hi,
Yes, from another functional component.
But now I have started a course in Udemy, because it is a hard stuff just to jim in it.
Thank you.

Yes, that makes sense. I guess React does not see "datasource" as a component and therefore you can't use a hook inside of it. But if you lear it bottom up with udemy thats definately a great idea!
Have fun!

 

Hi @nicomartin ,

Thanks for your great post. I would like to know, with the same sample code, in addition to the error message, how would you display a "Retry" button to fire a new API call?

 

Hi @mykel ,
Excellent question! In that case I think I would just move the loading logic into a new function that I can then return from the hook as well:
gist.github.com/nico-martin/24de58...