Since the introduction of React hooks, the creation and utilization of functional components became even more seamless. With useEffect
and useState
lifecycle methods previously available to class components are also available in functional components.
React's very purpose is to provide reusable blocks of code that form the various parts of an application. In this post, we'll explore how to use custom hooks to abstract component functionality reusable across a React application.
To follow through with this post, you should be conversant with React.js.
Why use custom hooks
You may wonder why you should even bother with writing custom React hooks when we can write your state and effect logic in the component and get to building the UI.
You are right.
It would be best if you mostly used a custom hook when you need to abstract a frequently used component functionality utilizing state and effect logic. Custom hooks are primarily present in large applications with multiple repetitive portions.
For example, in a b2b e-commerce marketplace application, you may require fetching order data or seller information in multiple components. You can handle this particular fetching operation every time a component requires the data, or you can make a hook to handle it. The same applies to fetching location data in an application where user location is required in multiple components. Here are some reasons why I use custom hooks in large projects:
- Provides useful abstraction as the same hook can be used across multiple components.
- Side effects like utility function calls, application state update, and singular hook requirements are managed independently with cleanups.
- You can use multiple hooks in one component without clutter.
- In Typescript, you want to have all the types in one place also, and not bloat the component code for readability.
If you don't have to handle this reusability, excuse my firm opinion, avoid the hasty abstraction, and don't use custom hooks.
Structure of custom hooks
Custom hooks are simply functions that encapsulate React useEffect
and useState
APIs.
They take parameters as specified and return data. The data could be an array, object, and primitive data types as specified.
Within the hook, all the magic happens. This hook is used across components. The result is a cleaner and well-organized codebase.
Here's what a custom hook looks like that fetches data for an order, showing the various parts in comments:
import { useEffect, useState } from "react";
// hook definition
function useGetOrder(input) {
const { id } = input;
// component state creation
const [orderId, setOrderId] = useState(id);
const [isLoading, setIsLoading] = useState(false);
const [hookData, setHookData] = useState(undefined);
// Function to run on first load
useEffect(() => {
setIsLoading(true);
// fetch data
const fetchData = async () => {
let orderData;
try {
orderData = await getOrder(orderId);
} catch (e) {
throw Error(e);
}
setHookData(orderData);
setIsLoading(false);
};
fetchData();
// handle cleanup
return async () => {
await unMountFn();
};
}, [orderId]);
// hooks return array
return [{ isLoading, hookData }, setOrderId];
}
// export hooks
export { useGetOrder };
From the snippet above, we can see the hook has the following parts:
- Module import (useState & useEffect)
- Function arguments restructuring
- State creation
- Component mount logic in useEffect
- Component unmount logic (returned in useEffect)
- Component update variable
- Hooks return data
- Hooks export
This hook depicts a data fetching operation on receipt/update of an input variable orderId
.
Instead of fetching data in useEffect
, you could use a web API to transform data, and you could store data in the application state (if it's a valid use case) or call a utility function.
Custom hooks in action
Below is the hook we shared earlier to fetch an order data in use. With a familiar file name of useGetOrder.js
, we have the following content:
import { useEffect, useState } from "react";
// API call to get data
async function getOrder(id) {
const res = await fetch("./order.json");
const data = await res.json();
return data;
}
// unmount Function
async function unMountFn(data) {
// handle any cleanup process
}
// hook definition
function useGetOrder(input) {
const { id } = input;
// component state creation
const [orderId, setOrderId] = useState(id);
const [isLoading, setIsLoading] = useState(false);
const [hookData, setHookData] = useState(undefined);
// Function to run on first load
useEffect(() => {
setIsLoading(true);
// fetch data
const fetchData = async () => {
let orderData;
try {
orderData = await getOrder(orderId);
} catch (e) {
throw Error(e);
}
setHookData(orderData);
setIsLoading(false);
};
fetchData();
// handle cleanup
return async () => {
await unMountFn();
};
}, [orderId]);
// hooks return array
return [{ isLoading, hookData }, setOrderId];
}
// export hooks
export { useGetOrder };
In the hook, we created functions to fetch data from a local json file, a function to be called on component destruction, and the hook's definition.
The hook function takes an input, and in the hook definition, we create state variables to hold the input data, loading state, and hooks data.
NB: The input data in this function is for reference and not utilized in the hooks logic
The hook returns an array containing an object in the first index to retrieve the loading state and the hook data. setOrderId
, which modifies the input data, is assigned the second index.
This hook is used in a component to fetch order data like this:
import React from "react";
import { useGetOrder } from "../hooks/useGetOrder";
const HomeOrder = () => {
const [{ isLoading, hookData }, setOrderID] = useGetOrder(123);
return (
<div>
<h3>Home Order</h3>
{isLoading && <p>Fetching order ⏳</p>}
{hookData && (
<div>
<p>ID: {hookData.id}</p>
<p>Payment Captured: {hookData.paymentCaptured ? "True" : "False"}</p>
<p>Amount: ${hookData.totalAmount}</p>
<p>Shipping Fee: ${hookData.shippingFee}</p>
<p>Shipping Address: {hookData.shippingAddress}</p>
<p>User ID: {hookData.userId}</p>
<h4>Order Items</h4>
{hookData.products.map((product, key) => (
<div key={key}>
<p>
{product.title} - ${product.price}
</p>
</div>
))}
</div>
)}
</div>
);
};
export { HomeOrder };
The data, once fetched, can be used in the component. Rather than have the full state and mount logic in the component, we now have it as a hook that can be used by multiple components.
NB: Just as you cannot use native React hooks outside of a React component, custom hooks containing
useState
anduseEffect
also cannot be used outside a react component
Here's the final Codesandbox with the demo.
For large projects, you could do several optimizations and customizations to improve user experience and flexibility. These include:
Having a wrapper for custom hooks with Types and generic configurations.
Abstracting mount, unmount, error, and loading functions as parameters in the hooks definition.
Summary
In this post, we saw how to create a custom hook to handle reusable component logic in a React app. We also learned why we use custom hooks and how custom hooks look.
To a better 2021, and happy new year!
William.
This article was originally published on Hackmamba
Top comments (1)
Why create one when you can get all awesome hooks in a single library?
Try scriptkavi/hooks. Copy paste style and easy to integrate with its own CLI