Custom React 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)
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