If you have worked with React, for any length of time, you would have come across hooks. You may have even used them in your React apps. If you used React from version 18+ onwards, you would have been exposed to hooks.
But, first, a thing or two about hooks ...
Hooks are reusable functions that you can create and then import and reuse in as many of your React components as you need. You can also call a hook from inside another hook.
A hook's name is typically prefixed with `use`. That's why some of the built-in React hooks are named: `useState`, `useEffect`, `useContext`, etc.
When you create your custom hook, it is also a great idea to name your hook by prefixing it with the word `use`. We will do that in a few moments.
There are many areas of use for a custom hook.
There are more use cases that you will come across as you build more complex React applications with more complicated components.
The main takeaway here is: If you have a need for a unit (or collection of units) of logic that you need to implement in multiple components, create a custom function (aka "hook") to handle that logic and then import and reuse that hook in as many components as you need.
There is a simple process you should follow to create a custom hook for your React app.
So, let's go into some details about each of the above steps in the hook creation process.
So, let's do that.
touch useFetchApi.js
We are creating a hook to help make CRUD calls to our backend API server using the `Fetch API`. So, we are (appropriately) calling the file `useFetchApi.js`.
Now, let's open the `useFetchApi.js` file (in your favorite code editor) and code its logic.
import { useState, useEffect } from 'react'
const useFetchApi = (apiUrl) => {
const [statusCode, setStatusCode] = useState('')
const [postData, setPostData] = useState(null)
const [apiError, setApiError] = useState('')
const fetchData = () => {
fetch(apiUrl)
.then(response => response.json())
.then(data => setPostData(data))
.catch(error => setApiError(`Error! No data found.`))
}
useEffect(() => {
fetchData()
}, [])
return {postData, apiError, statusCode}
}
export default useFetchApi
You wil notice that our custom hook:
- Needs to make use of React's built-in `useState()` and `useEffect()` hooks.
- Is a function named `useFetchApi()`
- Has a utility/worker function called `fetchData()`. This is where the actual workload of the function is. All the logic we need the `useFetchApi` hook to perform is done inside the `fetchData()` worker function.
- Triggers the `fetchData()` worker function to run by invoking the `fetchData()` function inside the `useEffect()` hook.
- Returns the `postData`, `apiError` and `statusCode` data from the `useFetchApi` hook. Wherever we call the `useFetchApi` hook, we will get the values of these three items. That's why we had the `return {postData, apiError, statusCode}` statement.
If we had `return`ed more objects (strings, arrays, functions or other types of data), we would have been able to get those data back as well. In order words: `garbage in, garbage out.` We will take a closer look at these as this developer guide progresses.
- Exports the custom hook itself so it can be imported into and used inside the components of our app. Hence the `export default useFetchApi` at the end of the file.
When you look closely at the hook, you will notice that it bears an uncanny resemblance to a React component.
The only noticeable difference here is that you are not returning any JSX (like a component would have done); you are, instead, returning some data.
At this point, we have successfully created a custom hook.
Now, let's put it to the test and see if actually works as we expect it to.
But, first, let's create a component where we will use the hook.
2. Use the Hook
Create a `components` folder inside the `src/` directory. Then, add a `Posts` component file (which will render a list of blog posts.).
So, basically:
cd src && mkdir components
cd components && touch Posts.js
Now, open that `components/Posts.js` file in your code editor and let's get to work building it.
After a few minutes of hard work, your `components/Posts.js` file should look like this:
import React, { useState, useEffect } from 'react'
import useFetchApi from '../hooks/useFetchApi'
const Posts = () => {
const [posts, setPosts] = useState([])
const [postError, setPostError] = useState('')
const {postData, apiError} = useFetchApi('http://localhost:3500/posts/')
useEffect(() => {
if(postData){
setPosts(postData)
}else {
setPostError(apiError)
}
}, [postData, apiError])
return (
<div>{posts.length > 0 ? ( <>
<h3> Latest Blog Posts
<div> {posts.map(post => (
<p><a href="{`posts/${post.id}`}"> {post.title} </a></p>
))}</div>
</> ) : ( <> Sorry, something went wrong. {postError} </> )}</div>
)
}
export default Posts
By looking at the `Posts` component's logic, you will notice that:
- We are importing our custom `useFetchApi` hook
- We are invoking the `useFetchApi` hook to get the values returned for its `postData` and `postError` data.
- We are using the `useEffect()` hook to set the values of the component state depending on which data was returned from the `useFetchApi` call.
- We are rendering the component state (set to the data returned from the `useFetchApi` hook's call) in the UI of the `Posts` component.
3. Refactor the Hook
If you look closely at our implementation of the `useFetchApi` hook, you will notice that:
- It only does GET calls by default.
- It needs to (but doesn't have the logic to) make POST, PUT and DELETE HTTP calls to the API backend.
So, let's take care of this issue by refactoring the hook to make full CRUD calls to the API backend.
To accomplish that, we will:
- Let the component pass the `method` for the HTTP call (from any of POST, GET, PUT or DELETE) instead of relying on the default (and implicit) GET method that we currently have.
- Make the `body` of the API call an optional parameter (so that it can be used when making POST, PUT or PATCH calls but not when making GET or DELETE calls).
When you look at the docs for the `
Fetch API` on the Mozilla Developer Network (MDN) docs, you will notice that `fetch()` can take two parameters:
- A url
- An options object: which can contain the `method` as well `body` properties for the API call. The `body` propery is needed when making a `POST` or `PUT` call.
Let's redesign our `useFetchApi` hook to take these parameters into consideration.
Update the `hooks/useFetchApi.js` file to reflect these changes:
import { useState, useEffect } from 'react'
const useFetchApi = (apiUrl, apiOptions) => {
const [statusCode, setStatusCode] = useState('')
const [postData, setPostData] = useState(null)
const [apiError, setApiError] = useState('')
const fetchData = () => {
fetch(apiUrl, apiOptions)
.then(response => response.json())
.then(data => setPostData(data))
.catch(error => setApiError(`Error! No data found.`))
}
useEffect(() => {
fetchData()
}, [])
return {postData, apiError, statusCode}
}
export default useFetchApi
From now on, our hook is versatile enough to be used to make POST, UPDATE (PUT or PATCH), GET or DELETE calls.
All the component would need to do is to pass in an `apiOptions` object (which could possibly be empty, so that the default `Fetch API` config object is used) when invoking the `useFetchApi` hook.
If it is a POST, PUT or PATCH call, the host component would need to send in both the `method` and `body` properties of the `apiOptions` object to make the call valid. Otherwise, it reverts to a GET call (the default `method`) with no/empty `body` property.
Best Practices for Creating Custom React Hooks
As you develop more complex React hooks and read other people's code, you will notice "standard practices" and "best approaches" to developing hooks. Some will match what the general React developer community does. Others will be "innovative" (as per your or other people's standards.)
But, for now, here are a few best practices to keep in mind as you develop your own custom React hooks for your apps.
- `Modularity`: Don't just create a `myHooks.js` file somewhere in your `src` folder. Even if you are creating only one simple hook, create a folder to hold it and any future custom hooks you will create. Call the folder `hooks` or something that immediately reminds or informs a developer that the folder contains files for your custom hooks.
Then, create a file for each of your custom hooks. Don't just put all your hooks in one bulky file. Make it easy for you (and your fellow developers) to debug and write tests for your (hooks) code without having to get on a call whenever they need to touch your app's codebase.
- `Breakdown and Simplify`: If you need to create a hook to make API calls (for some financial accounts) as well as calculate the balances of those accounts and return the `totalExpenses`, `totalIncome`, `accountBalance` and such, consider breaking your monolithic hook into two hooks: one to make the API calls and return the HTTP response data or error; and, another hook to do the calculations and return the resulting total values for your income and expenses.
- `Declutter your Code`: If you notice that your component's logic is doing more than you need it to do or you have more than one component having to perform the same task, consider putting that extra or shared workoad into a custom hook that you can then call from the components that need it. This also pays homage to the `DRY` ("Don't Repeat Yourself") principle.
- `Refactor without Fear`: There is no such thing as "perfect code". Well, at least, not perpetually perfect.
If you find that a great piece of code you wrote (for your custom hooks or whatever else) needs to be updated/changed, please, don't hesitate to add, remove or change your code's logic to make it better, more versatile or do more (with less) code.
There are more best practices. But these will do for now.
Keep track of what other best practices you notice as you grow in experience as a developer or through your interactions with both new and experienced developers at work or in the developer communities you find yourself in.