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
Top comments (12)
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!
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.
you got hooked!
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. :(