loading...
Cover image for React: Writing a custom API hook

React: Writing a custom API hook

patrixr profile image Patrick R ・3 min read

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);

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();

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);

}

Calling the service

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

useEffect(() => {
   // ... async code
}, []);

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() }, []);

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() }, []);

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 ];
}

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>
  );
}

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

Posted on by:

patrixr profile

Patrick R

@patrixr

I love tech, guitars and climbing. I have a compulsive tendency to rant about coding

Discussion

markdown guide
 

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

 

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 :)

 

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

 
 

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

 

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.

 

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

 

all code shared gist pls...

 

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

 

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.

Thanks Rafael !

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

 

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