DEV Community

Cover image for React: Writing a custom API hook
Patrick R
Patrick R

Posted on

React: Writing a custom API hook

Let's write a handy custom react hook to take care of the usual API logic we've all written time and time again.

Introduction

After a couple of years away from React, I'm re-educating myself on the best practices. This means : Hooks

One of the very (very) common flow we find across our apps is that of loading data from the API and displaying it.

It usually looks somewhat like this :

This has a tendency to result in very cluttered components. Let's use our newfound knowledge of hooks to solve this.

Designing the hook

Based on the flow described above, it's pretty easy to define the data that we want our hook to provide. It will return :

  • The response data
  • A loading flag
  • An error (nulled on success)
  • A retry method

Given that I still appreciate delegating the request code to a service class, my thought is to have the hook call the service.

Leading to the following usage:

const [ user, isLoading, error, retry ] = useAPI('loadUserById', 56);
Enter fullscreen mode Exit fullscreen mode

Preparing the API service

Let's use a little service class, in which we can place all of our beautiful ajax code.

class APIService {
    async loadUsers() {
        // ... ajax magic
    }

    async loadUserById(id) {
        // ... ajax magic
    }
} 

export default new APIService();
Enter fullscreen mode Exit fullscreen mode

Writing the hook

Our goal here is simply to combine standard react hooks to create all of our required fields.

The state

React already provides us with the useState hook to create and update state properties.

Let's generate our fields :

function useAPI(method, ...params) { 

  const [data, setData]           = useState(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, onError]          = useState(null);

}
Enter fullscreen mode Exit fullscreen mode

Calling the service

The React hook that comes in play here is useEffect, in which we can run our asynchronous code.

useEffect(() => {
   // ... async code
}, []);
Enter fullscreen mode Exit fullscreen mode

However, we've decided that the hook would return a retry method. So let's move the asynchronous code to its own function

const fetchData = async () => {
   // ... async code
}

useEffect(() => { fetchData() }, []);
Enter fullscreen mode Exit fullscreen mode

Let's now call the correct service method, based on the hook's arguments

const fetchData = async () => {
  // Clear previous errors
  onError(null);

  try {
    // Start loading indicator
    setIsLoading(true);

    // Fetch and set data
    setData(await APIService[method](...params));
  } catch (e) {
    // Set the error message in case of failure
    setError(e);
  } finally {
    // Clear loading indicator
    setIsLoading(false);
  }
};

useEffect(() => { fetchData() }, []);
Enter fullscreen mode Exit fullscreen mode

Result

And voila ! Our hook is ready for consumption.

function useAPI(method, ...params) {
    // ---- State
    const [data, setData]           = useState(null);
    const [isLoading, setIsLoading] = useState(false);
    const [error, setError]         = useState(null);

    // ---- API
    const fetchData = async () => {
      onError(null);
      try {
        setIsLoading(true);
        setData(await APIService[method](...params));
      } catch (e) {
        setError(e);
      } finally {
        setIsLoading(false);
      }
    };

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

    return [ data, isLoading, error, fetchData ];
}
Enter fullscreen mode Exit fullscreen mode

Usage in a component

Let's write a little example of how that might be used in a component

function HomeScreen() {
  const [ users, isLoading, error, retry ] = useAPI('loadUsers');

  // --- Display error
  if (error) {
    return <ErrorPopup msg={error.message} retryCb={retry}></ErrorPopup>
  }

  // --- Template
  return (
    <View>
      <LoadingSpinner loading={isLoading}></LoadingSpinner>
      {
          (users && users.length > 0) &&
            <UserList users={users}></UserList>
      }
    </View>
  );
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

There are many ways to avoid re-writing common code across the application.

In the past I've often delegated some of that to a Store, or used Mixins to create components with all that logic ready to use.

Custom hooks give us a whole new flavour and open up new strategies for dealing with problems.

Happy to witness the evolution of practices.

Cheers,

Patrick

Top comments (12)

Collapse
 
diogomqbm_ profile image
Diogo Mafra • Edited

Why did you decided to use APIService abstraction as a class? Couldn't you make an object with each ajax magic on it? That way you could follow your functional approach. Congratz on the post! Awesome abstraction, ty for sharing!


const APIService = {

  loadUsers: async () => {
     // ajax magic
  },

  loadUsersById: async (id) => {
     // ajax magic
  }

}

export default APIService

Collapse
 
patrixr profile image
Patrick R

Valid point, in part a force of habit, and in another I do have a tendency to inherit base service classes, as seen here. That part kinda got lost as I was trying to simplify things.

Glad you like the hook :)

Collapse
 
marcoscosta profile image
Marcos Costa

Very nice approach Patrick, I'll definitely give it a try, using @diogomqbm_ suggestion as well.

Collapse
 
habelfoc profile image
Habel Justin

you got hooked!

Collapse
 
delimanicolas profile image
Nicolas Lima

Nice post! I'll definitely adopt pieces of your structure. @diogomqbm_ I will use your approach as well.

Collapse
 
robflowk profile image
Robin Wloka

this is a great way of fixing a very common problem i've seen so many times. I really like your approach and i think i'll try it in my current react project.
Hooks are such a blessing.

Collapse
 
patrixr profile image
Patrick R

Hooks are a blessing indeed ! Glad you like it :)

Collapse
 
ahmetozalp profile image
Ahmet

all code shared gist pls...

Collapse
 
patrixr profile image
Patrick R

The entirety of the code is in the article. It's a single function :)

Collapse
 
rafaelfigueiredol profile image
Rafael Figueiredo Gomes Luz • Edited

Hi Patrick! Thanks for sharing this knowledge. I would be glad as well to see this at github. I'm having some problems trying to replicate this and split to use with imports.

Thread Thread
 
patrixr profile image
Patrick R

Thanks Rafael !

Here's a link to this hook on Github.
Cheers :)

Collapse
 
jivkojelev91 profile image
JivkoJelev91

Nice article. I would like to see a custom hook with post request. :(