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 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 it's 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