Building Powerful React Components with Custom Hooks
Infrasity Learning
Posted on May 23, 2024
If you've been working with ReactJS, you're likely familiar with hooks like useState for managing state and useEffect for handling side effects.
While these built-in hooks are great for smaller applications, as projects grow, complex logic can get repeated across components, leading to messy code.
This blog will guide you through the elegant and powerful concept of React called "Custom Hooks."
We at Infrasity specialize in crafting tech content that informs and drives a ~20% increase in traffic and developer sign-ups. Our content, tailored for developers and infrastructure engineers, is strategically distributed across platforms like Dev.to, Medium, and Stack Overflow, ensuring maximum reach. We deal in content & (Go-To-Marketing)GTM strategies for companies like infrastructure management, API, DevX, and Dev Tools. Weβve helped some of the worldβs best infrastructure companies like Scalr, Env0, Terrateam, Terramate, Devzero, Kubiya, and Firefly.ai, just to name a few, with tech content marketing.
What are Custom Hooks in React? π€
Custom Hooks are a way to encapsulate reusable logic into functions specifically designed for React components.
They leverage the power of built-in hooks like useState and useEffect to create functionality that can be shared across different components within your React application.
While React Hooks are a way to manage state and side effects in React components, custom hooks are a way to write reusable logic that can be used across different components and even outside of React.
How are Custom Hooks different from Built-in React Hooks?
If you are familiar with useState
, useEffect
, useCallback
... you already know what hooks are in react.
If you don't, learn more about hooks in react by clicking here.
Built-in React Hooks
Built-in hooks like useState and useEffect provide ways to manage state and side effects within React components.
They handle things like data updates and side effects (like fetching data) that can't be done directly in functional components.
Custom Hooks
These are reusable functions you create that encapsulate logic involving React Hooks. They allow you to break down complex functionality into smaller, more manageable pieces that can be reused across different components.
Note:
Both Custom Hooks and React Hooks (built-in or custom) follow the Rules of Hooks. This means they can only be called at the top level of a React component, outside of conditional statements or loops.
Benefits of Custom Hooks:
- Code reusability: Share common logic across components without prop drilling or higher-order components.
- Improved code organization: Break down complex logic into smaller, more focused units.
- Better readability: Make code easier to understand and maintain.
Let's delve into the realm of Custom Hooks and unlock their full potential in your development journey with the help of an example.
The Demo App π₯
We made a demo app (an Image Gallery app in this case) that fetches images from Lorem Picsum and displays them in the app. Feel free to clone this repository for the code of our demo project.
There's also a navigation that you can use to navigate (go to the next page and the previous one).
For simplicity's sake, I kept the UI simple and didn't use react-router
in the example project.
Infrasity has made a lot of similar demo recipe libraries to assist Devzero, one of our customers, in creating recipe libraries, which assisted them in quick developer onboarding. The recipe libraries created were a combination of different tech stacks, like NodeJs, BunJS, React, and CockroachDB, so that the end users could directly use them as boilerplate code without writing the entire code from scratch. This has assisted both our customers with user signups and the end users in increasing their development productivity. Samples of the same canΒ beΒ viewedΒ here.
Note: the recording is slowed down so you can see the following:
- Loader (appears when images are being fetched)
- Error Message (if images are failed to fetch)
Prerequisites
- Node.js - Version v21.6.2
- npm - Version 10.2.4
- React.js - Version 16 and above
Setting up the environment
Navigate to the project folder and run the following commands via the terminal
# To install the dependencies (run only once)
npm i
# To start the web app server
npm run dev
You'll see something like this
VITE v5.2.11 ready in 234 ms
β Local: http://localhost:5173/
β Network: use --host to expose
β press h + enter to show help
Why Custom Hooks? Ditching Component Clutter π§
Traditional Components: A Messy Mix
Let's examine how the Image Gallery App currently works to understand why we need to use custom hooks in the first place!
Directory Structure of the Image Gallery App
βββ README.md
βββ index.html
βββ package.json
βββ public
βββ src
β βββ App.css
β βββ App.jsx
β βββ assets
β β βββ loaderAnimation.svg
β βββ components
β β βββ ErrorMessage.jsx
β β βββ Header.jsx
β β βββ Image.jsx
β β βββ Images.jsx
β β βββ Loading.jsx
β βββ index.css
β βββ main.jsx
β βββ util
β βββ http.js
βββ vite.config.js
6 directories, 15 files
App.jsx
import { useCallback, useState } from "react";
import Images from "./components/Images";
import Header from "./components/Header";
import "./App.css";
function App() {
const [pageNumber, setPageNumber] = useState(1);
const [hideNavigation, setHideNavigation] = useState(true);
const handleNextPage = () => {
setPageNumber((prev) => prev + 1);
};
const handlePrevPage = () => {
setPageNumber((prev) => prev - 1);
};
const handleHideNavigation = useCallback((hideNav) => {
setHideNavigation(hideNav);
}, []);
return (
<>
<Header
currentPage={pageNumber}
handleNextPage={handleNextPage}
handlePrevPage={handlePrevPage}
hideNavigation={hideNavigation}
/>
<Images
pageNumber={pageNumber}
setHideNavigation={handleHideNavigation}
/>
</>
);
}
export default App;
What's happening in the App.jsx?
- We are using the
useState
hook to define states for the current page number and hiding navigation. - We are using the
useCallback
hook to define a function that toggles the hide navigation state so it never gets redefined whenever component re-renders. - We are returning
Header
andImages
custom components and passing some props for Navigation.
App.css
@import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap");
* {
font-family: "Poppins", sans-serif;
}
header {
padding: 1rem;
text-align: center;
}
.images-section {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 1rem;
}
@media (max-width: 768px) {
.images-section {
grid-template-columns: repeat(2, 1fr);
}
.image {
margin: 1rem;
padding: 1rem;
}
.image a {
overflow: hidden;
}
.image img {
max-width: 15rem;
}
}
@media (max-width: 480px) {
.images-section {
grid-template-columns: repeat(1, 1fr);
margin: 0;
}
.image {
width: 10rem;
}
.image img {
max-width: 10rem;
}
}
.image {
text-align: center;
border: 1px solid black;
border-radius: 15px;
padding: 2rem;
margin: auto;
width: 75%;
margin-bottom: 2rem;
}
.image img {
width: 15rem;
box-shadow: 2px 4px 4px black;
}
.author {
font-weight: 500;
}
.dimensions {
font-style: italic;
}
button,
.download {
margin: 1rem;
padding: 1rem;
border-radius: 5px;
border: none;
background-color: black;
color: white;
text-decoration: none;
font-weight: 600;
}
.download-wrapper {
margin: 2rem;
}
.loading,
.error {
text-align: center;
font-weight: bold;
}
.error,
.loading {
width: 30rem;
padding: 2rem;
margin: auto;
border: 10px solid black;
background-color: black;
color: white;
}
.loading img {
width: 7em;
}
Header.jsx
function Header({
currentPage,
handleNextPage,
handlePrevPage,
hideNavigation,
}) {
return (
<header>
<h1>Image Gallery</h1>
{!hideNavigation && (
<nav>
<h2>Page: {currentPage}</h2>
<button onClick={handlePrevPage}>Previous</button>
<button onClick={handleNextPage}>Next</button>
</nav>
)}
</header>
);
}
export default Header;
What's happening in the Header.jsx?
- We are used the props passed from
App.jsx
into the Header and triggering thehandlePrevPage
andhandleNextPage
functions on button click. - Hiding the navigation based on the
hideNavigation
value. - Displaying the page number using
currentPage
prop.
Images.jsx
import { useEffect, useState } from "react";
import { fetchImages } from "../util/http";
import Loading from "./Loading";
import ErrorMessage from "./ErrorMessage";
import Image from "./Image";
function Images({ pageNumber, setHideNavigation }) {
const [images, setImages] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
useEffect(() => {
async function fetchImageData() {
setIsLoading(true);
try {
const fetchedImages = await fetchImages(pageNumber, 10);
if (fetchedImages.length > 0) {
setImages(fetchedImages);
setHideNavigation(false);
} else {
throw new Error("Something went wrong while fetching images");
}
} catch (e) {
console.log(e);
setIsError(true);
setErrorMessage(e.message);
}
setIsLoading(false);
}
fetchImageData();
}, [pageNumber, setHideNavigation]);
let finalContent = null;
if (isLoading) {
finalContent = <Loading />;
} else if (isError) {
finalContent = <ErrorMessage>{errorMessage}</ErrorMessage>;
} else {
finalContent = (
<section className="images-section">
{images.map((image) => (
<Image key={image.id} image={image} />
))}
</section>
);
}
return <>{finalContent}</>;
}
export default Images;
What's happening in the Images.jsx?
- States are defined using
useState()
hook and some helper functions are defined for changing the state resulting in change in page numbers and toggling navigation. - We are using the
useEffect
hook to fetch images from an API when the component mounts. - We are passing
isLoading
,isError
, anderrorMessage
states to theLoading
,ErrorMessage
, andImage
components respectively, so that the consuming component can conditionally render the appropriate content based on the state. - The images are fetched from the API using the
fetchImages
function fromhttp.js
which takes two parameters -page
andlimit
- and returns a list of images.
http.js
export const fetchImages = async (page = 1, limit = 10) => {
const response = await fetch(
`https://picsum.photos/v2/list?page=${page}&limit=${limit}`
);
if (!response.ok) {
throw new Error("Failed to fetch images");
}
const imageList = await response.json();
return imageList;
};
What's happening in the http.js?
The
http.js
module is a helper module that exports a single function,fetchImages
, which fetches images from the Lorem Picsum API and returns a list of images.The
fetchImages
function takes two parameters,page
andlimit
, and uses them to construct the URL for the API request. Thepage
parameter defaults to1
and thelimit
parameter defaults to10
.The
fetchImages
function then returns a list of images from the API response.
Header.jsx
function Header({
currentPage,
handleNextPage,
handlePrevPage,
hideNavigation,
}) {
return (
<header>
<h1>Image Gallery</h1>
{!hideNavigation && (
<nav>
<h2>Page: {currentPage}</h2>
<button onClick={handlePrevPage}>Previous</button>
<button onClick={handleNextPage}>Next</button>
</nav>
)}
</header>
);
}
export default Header;
What's happening in the header.jsx?
- The
Header.jsx
component renders a header with a navigation bar that shows the current page number and two buttons for navigating to the previous and next pages. - The navigation bar is conditionally rendered based on the
hideNavigation
prop. - If
hideNavigation
istrue
, then the navigation bar is hidden.
ErrorMessage.jsx
function ErrorMessage({ children }) {
return (
<div className="error">
<h2>Error β οΈ</h2>
<p>{children}</p>
</div>
);
}
export default ErrorMessage;
What's happening in the ErrorMessage.jsx
?
- The
Error.jsx
component renders a error message box with a header and a paragraph that displays the error message. - The
Error.jsx
component is conditionally rendered by theImages.jsx
component based on theisError
state.
Loading.jsx
import loaderAnimation from "../assets/loaderAnimation.svg";
function Loading() {
return (
<div className="loading">
<img src={loaderAnimation} alt="Loading animation" />
<p>Loading content. Please wait...</p>
</div>
);
}
export default Loading;
What's happening in the Loading.jsx
?
The power of separation of logic from components using custom hooks is that components can continue to render while the hook is performing its side effects, without blocking the rest of the application.
This is because the hook is not a part of the component's render method, but is instead called when the component is mounted or updated. This allows the component to continue rendering while the hook is performing its side effects, without blocking the rest of the application.
The Problem
The Images.jsx
component has now become a FAT component and the logic used for fetching images, setting loading and error messages are not reusable for any other component!
This is because the Images.jsx
component is responsible for rendering the images, handling the loading state, and handling errors, all while also fetching the images from the API.
If we were to use this component in another part of the app, we would have to repeat the same logic for handling the loading state and errors, which is not very DRY (Don't Repeat Yourself).
Oh No! meme | Credit: Tenor
The Power of Separation: Enter Custom Hooks
Fly Sky GIF | Credit: Tenor
Custom hooks are a way to encapsulate reusable logic in a function, making it easy to share and reuse code across different components.
By using custom hooks, you can write more organized and maintainable code, as the logic for fetching images, setting loading and error messages, and other features are not repeated across components.
Instead, you can write the logic once and share it across all necessary components. This makes your code more DRY and helps to avoid code duplication.
Let's learn how to improve the above code using React custom hooks!
Building Your First Custom Hook: A Step-by-Step Guide βοΈ
Diving into the Essentials
Naming Conventions
Custom hooks in React should start with the prefix use
. This is a convention established by the React team to differentiate custom hooks from regular functions.
File Naming Conventions
The filename of a custom hook should also start with the prefix use
. This makes it easy to identify custom hooks in your codebase.
For example, a custom hook for fetching images should be named useFetchImages.js
.
Functionality
Custom hooks can encapsulate any type of logic that is reusable across components. This includes functionality for managing state, performing side effects, and handling subscriptions to external data sources.
Returning Values
Custom hooks can return any type of value to consuming components, including data, functions, and objects. This allows the consuming component to access the returned value and use it as needed.
Crafting a Simple Hook: Fetching Data on Demand π‘
Step 1: Create a hooks directory inside src directory
Step 2. Create a file named useFetchImages.js
Step 3. Move the logic from Images.jsx
that managed the state for fetching, loading, and showing error
Updated useFetchImages.js
import { useEffect, useState } from "react";
import { fetchImages } from "../util/http";
export default function useFetchImages(pageNumber, setHideNavigation) {
// The Logic for fetching images and setting it to the state
const [images, setImages] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
useEffect(() => {
async function fetchImageData() {
setIsLoading(true);
try {
const fetchedImages = await fetchImages(pageNumber, 10);
if (fetchedImages.length > 0) {
setImages(fetchedImages);
setHideNavigation(false);
} else {
throw new Error("Something went wrong while fetching images");
}
} catch (e) {
console.log(e);
setIsError(true);
setErrorMessage(e.message);
}
setIsLoading(false);
}
fetchImageData();
}, [pageNumber, setHideNavigation]);
return { images, isLoading, isError, errorMessage };
}
Updated Images.jsx
/* eslint-disable react/prop-types */
import Loading from "./Loading";
import ErrorMessage from "./ErrorMessage";
import Image from "./Image";
// Our custom hook being imported
import useFetchImages from "../hooks/useFetchImages";
function Images({ pageNumber, setHideNavigation }) {
// Moved the logic into a custom hook
const { images, isLoading, isError, errorMessage } = useFetchImages(
pageNumber,
setHideNavigation
);
let finalContent = null;
if (isLoading) {
finalContent = <Loading />;
} else if (isError) {
finalContent = <ErrorMessage>{errorMessage}</ErrorMessage>;
} else {
finalContent = (
<section className="images-section">
{images.map((image) => (
<Image key={image.id} image={image} />
))}
</section>
);
}
return <>{finalContent}</>;
}
export default Images;
Changes to Images.jsx
The custom hook has significantly reduced the complexity of the Images.jsx
component by encapsulating the logic for fetching images, managing loading and error states, and defining the errorMessage
state.
The component is now more concise and easier to maintain, as it conditionally renders the states returned by the hook.
Common Use Cases of Custom Hooks π₯
The Reusability Advantage
In the above code, we created a custom hook for fetching images, making the Images component shorter, easier to read, and more maintainable.
Question of the day! Can we do BETTER? π€
I took the liberty to make the custom hook and the consumer component more generalized to make it more reusable.
Updated useFetchImages.js
import { useEffect, useState } from "react";
export default function useFetchData(fetchFunction, pageNumber, callback) {
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
useEffect(() => {
async function fetchData() {
setIsLoading(true);
try {
const fetchedData = await fetchFunction(pageNumber, 10);
if (fetchedData.length > 0) {
setData(fetchedData);
callback();
} else {
throw new Error("Something went wrong while fetching data");
}
} catch (e) {
console.log(e);
setIsError(true);
setErrorMessage(e.message);
}
setIsLoading(false);
}
fetchData();
}, [pageNumber, callback, fetchFunction]);
return { data, isLoading, isError, errorMessage };
}
The Difference:
The main difference between the useFetchImages
and the useFetchData
...?
The useFetchData
function is more generic and reusable, as it accepts a
callback function and a fetch function as parameters, making it more versatile
and applicable to different use cases.
I updated the filename to
useFetchData.js
and changed the way it was being consumed in theImages
component.
function Images({ pageNumber, setHideNavigation }) {
// Added useCallback hook to avoid unnecessary re-renders
const unhideNavbar = useCallback(() => {
setHideNavigation(false);
}, [setHideNavigation]);
// Using the new generalized useFetchData hook
const {
data: images,
isLoading,
isError,
errorMessage,
} = useFetchData(fetchImages, pageNumber, unhideNavbar);
let finalContent = null;
if (isLoading) {
// Rest of the logic remains unchanged
Congratulations!!! π
We just made our custom hook generalized and hence; more reusable!
Hooray GIF | Credit: Tenor
Conclusion: Amplify Your React Development with Custom Hooks
Just like having the right tool for the job makes things easier, understanding the problem helps you choose the right tool in coding.
Remember that clunky "Images" component from before?
Using a custom hook totally transformed it! For example, it might have meant way less copy-pasting of code, making the whole thing easier to understand.
The big idea here is that Custom Hooks can seriously up your React coding game. By using them, you can write code that's cleaner, easier to maintain, and, best of all, reusable across different parts of your app. It's like having a toolbox full of pre-built solutions for common problems!
Infrasity assists organizations with creating tech content for infrastructural organizations on Kubernetes, cloud cost optimization, DevOps, and engineering infrastructure platforms.
We specialize in crafting technical content and strategically distributing it on developer platforms to ensure our tailored content aligns with your target audience and their search intent.
For more such content, including Kickstarter guides, in-depth technical blogs, and the latest updates on cutting-edge technology in infrastructure, deployment, scalability, and more, follow Infrasity.
Contact us at contact@infrasity.com today to help us write content that works for you and your developers.
Cheers! π₯
FAQs
1. What are React Custom Hooks?
React Custom Hooks are reusable functions that start with "use". They let you extract logic (like fetching data or managing state) from a component and share it across others. This keeps components clean and organized, promoting better code maintainability.
2. When to Use Custom Hooks?
When you have logic that's used in multiple components, create a Custom Hook to avoid code duplication.
For complex state handling within a component, a Custom Hook can break down the logic into smaller, more manageable units.
Custom Hooks can effectively manage side effects like data fetching (we saw above), subscriptions, or timers within functional components.
3. Rules for Using Custom Hooks:
Only call hooks at the very beginning of your custom hook function, before any conditional statements or returns.
Don't call hooks inside loops, ifs, or other conditional logic. React depends on the order hooks are called.
Custom hooks can only be called inside React function components, not class components.
4. Best Practices for Custom Hooks:
Each hook should handle a specific piece of logic, making it reusable and easier to understand.
Use usePascalCase with a clear description of the hook's function (e.g., useFetchData).
Rely on React's built-in hooks whenever possible. Consider external libraries for complex logic.
Return only the state or functions needed by the component using the hook.
Write unit tests to ensure your custom hooks function as expected.
5. Examples of Common Custom Hooks:
useFetchData
: Fetches data from an API and manages loading/error states.useLocalStorage
: Provides access to browser local storage with get/set functionality.useForm
: Handles form state, validation, and submission logic.useAuth
: Manages user authentication and authorization states.
Written by:
Gagan Deep Singh
for Infrasity
Posted on May 23, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.