Updated: Jul 4
Encapsulate fetch calls, logic, caching, and component state, all inside a custom React Hook
Custom React hooks can help us with some very important principles of good software architecture, such as code readability, separation of concerns, and avoiding code duplication. In this article we'll see how we can encapsulate fetch calls, logic, caching, and component state, all inside a custom React Hook.
React Hooks allow us to use the component’s state, to hook into the component’s lifecycle, and to use other React features in functional components, like we usually do in class components.
A RESTful application use case
Let’s say we are writing a RESTful application, which is a very common use case in web development. We need to interact with an API through HTTP requests, and therefore we need to handle asynchronous calls. In addition, we need to manage data on the client side, as well as keep it cached and updated. And last but not least, we need to use the component’s state and some lifecycle hooks if we want our component to be hooked with the data and the UI to be up to date and reactive.
If we choose to use functional components in our application, one option will be to do everything inside the component function. But then we will have all the disadvantages we’ve discussed - a very long function with many react hooks, mixed with logic and UI, and the worst thing is that maybe other functional components will have duplicated code (which comes with well known problems of code duplication, like a bug that needs to be fixed in many places instead of just one).
Therefore, another solution might be to build a custom hook to encapsulate it all and to be reused in our application.
Building our custom hook
Hooks always start with the word use.
For example, some built-in React Hooks are useState, useEffect, useRef, and useContext.
And so it is considered best practice to use the same convention in custom hooks.
This convention is also used in order to enforce the “Rules of Hooks”: https://www.npmjs.com/package/eslint-plugin-react-hooks
In addition, we might want to define our hook in its own file for better code separation and readability.
In this example we’ll call our hook useApi.
It will receive a URL as an argument and will return 3 things for the component to use:
data - an array that holds our data, and is hooked with the component’s state.
isQuerying - a boolean value indicating whether we’re waiting for an HTTP response, also hooked with the component’s state.
api - an object containing the CRUD operations.
Now, in order for our component to re-render when data changes, we need to use a built-in hook, the useState:
This adds data to our state, with an initial value of an empty array.
useState returns two values: a pointer to the value in the state, and a setter function that starts a component lifecycle when we use it and re-renders our component, similar to using setState in class components.
We can’t use a regular variable here, as changing it won’t re-render our component. The state is the place to store a value that will be available in the context of each component’s instance and that will trigger the component’s lifecycle when being changed.
Read more about useState in React documentation:
Now we want to start adding the CRUD operations.
Let’s start with our GET request that fetches our data.
Notice that our list function has no return value, and when we execute it in our component we can’t use await or then. Instead, we’re using the setData setter we got from useState, which triggers the component’s render lifecycle. The response will then be available for us in the new render via the data object we got from useState and which we exposed in our custom hook.
We also want to have a loader in the application which will be shown when we wait for the HTTP requests, and maybe to block the user from making another request before receiving the response from the previous one.
In order to achieve that we can use another boolean in the component’s state:
Our hook is still missing the POST/PUT/DELETE functions.
We will add them to the api object:
So we encapsulated all the logic, fetch calls, data, and state manipulations inside a custom hook.
Let’s see how we can use it...
Using our custom hook
Let’s say we are building a todo app and have a REST API we use to interact with the server.
For my example I built a serverless backend using Wix Code (Velo) that allows me to expose a REST api. Currently it only returns random mock data.
So let’s start with a functional component that uses our custom hook, giving it our base URL:
As we can see, we take the 3 exposed arguments from the useApi hook in order to later use them in our component.
We use the isQuerying boolean in order to show a loader on top of the list when we wait for a response from the server, and we use data in order to render the list of items.
If we want to fetch the data of the todo list on first render, we will use the useEffect hook.
With useEffect built-in hook, we can hook into the component’s lifecycle (like componentDidMount and componentDidUnmount in class components). We can pass an array of dependencies to useEffect (from state or props) to be used inside the hook’s callback. When passing an empty array the function will run on the first render only. We’ll pass our component’s props so the function will run every time a prop changes.
Let’s use it - and in its function we will call our api.list:
Read more about useEffect in React documentation:
We can also react to events in our application and then use our other api functions.
If we want to allow adding new items, we will need to use the useRef built-in hook, which will be connected to an input element:
And when the form is submitted we will call our api.add function:
We can also use our api.edit and api.remove functions in order to edit or remove items from the list:
* In the edit action we just added “edited” to the title for now.
Reuse our custom hook
After creating our useApi custom hook and using it in one component, we’ve already improved the design of our application.
Our component is cleaner - it has less hooks and logic in it.
And we have our hook in a separate file, handling REST API stuff only, making our code easier to read and understand.
As we mentioned before, the next step will be to use our custom hook in other components in our application.
That way we will see less code duplication and a faster development process - since we wrote the logic once, using it again becomes very easy.
Until now we handled the CRUD operations on items of a single todo list.
But let’s say we want to add the option to manage multiple lists to our application.
We can use our useApi hook again, but with a different URL, so the CRUD operations will be executed against another API.
This is the skeleton of our second component:
As we can see in the second component, using our common hook was very easy and only required a URL change.
Adding a cross-application feature
So reusing our custom hook in a new component is very easy, but let’s look at another aspect, an additional advantage of having the common code in one place.
Let’s say we need to change all the requests to the server so that they all contain an Authorization header.
Even if we already use our custom hook in dozens of components across the application, the change we need to make is in one place only - our file with the custom hook.