Building Powerful React Components with Custom Hooks
Tags: react, reacthooks, customhooks, frontend
Published: 2024-05-23 09:18:07

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**."
![Infrasity banner](https://media.licdn.com/dms/image/D4D3DAQHFzUxVo6jHBg/image-scale_191_1128/0/1694951147476/infrasity_cover?e=2147483647&v=beta&t=oS_E6ixVpThzWAJQ0oYxZouD2bRM-8H_zImyxeYe-Wo)
We at [Infrasity](infrasity.com) 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](dev.to), [Medium](medium.com), and [Stack Overflow](stackoverflow.com), 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](scalr.com), [Env0](env0.com), [Terrateam](terrateam.io), [Terramate](terramate.io), [Devzero](devzero.io), [Kubiya](kubiya.ai), and [Firefly.ai](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](https://legacy.reactjs.org/docs/hooks-intro.html).

## 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](https://picsum.photos/) and displays them in the app. Feel free to clone [this repository](https://github.com/ScaleupInfra/react-hooks-blog) 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.

![Demo of Image Gallery App](https://github.com/ScaleupInfra/react-hooks-blog/blob/main/blogs/react-custom-hooks/assets/Image%20Gallery%20App%20Demo.gif?raw=true)

[Infrasity ](infrasity.com)has made a lot of similar demo recipe libraries to assist [Devzero](devzero.io), 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:**

1. **Loader** (appears when images are being fetched)
2. **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

```bash
# To install the dependencies (run only once)
npm i

# To start the web app server
npm run dev
```

#### You'll see something like this

```bash
  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

```zsh
├── 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`

```js
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` and `Images` custom components and passing some props for Navigation.


#### `App.css`

```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`

```js
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 the `handlePrevPage` and `handleNextPage` functions on button click.
- Hiding the navigation based on the `hideNavigation` value.
- Displaying the page number using `currentPage` prop.

#### `Images.jsx`

```js
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`, and `errorMessage` states to the `Loading`, `ErrorMessage`, and `Image` 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 from `http.js` which takes two parameters - `page` and `limit` - and returns a list of images.

#### `http.js`

```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](https://picsum.photos/) and returns a list of images.

- The `fetchImages` function takes two parameters, `page` and `limit`, and uses them to construct the URL for the API request. The `page` parameter defaults to `1` and the `limit` parameter defaults to `10`.

- The `fetchImages` function then returns a list of images from the API response.

#### `Header.jsx`


```js
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` is `true`, then the navigation bar is hidden.

#### `ErrorMessage.jsx`

```js
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 the `Images.jsx` component based on the `isError` state.

### `Loading.jsx`

```js
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)](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself).

![Oh No! meme](https://media.tenor.com/PqgTNvSN8wIAAAAe/kermit-worried.png)

_Oh No! meme | Credit: Tenor_

### The Power of Separation: Enter Custom Hooks

![Fly Sky GIF](https://media1.tenor.com/m/dSnqhiv3sgkAAAAC/fly-sky.gif)

_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`**

```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`**


```js
/* 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`**

```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 the `Images` component.

```js
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](https://c.tenor.com/Iccl_wfwIdwAAAAC/tenor.gif)

_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](infrasity.com) 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](infrasity.com).

Contact us at [contact@infrasity.com](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
](https://www.linkedin.com/in/gagan-gulyani) for [Infrasity](www.infrasity.com)