Building Powerful React Components with Custom Hooks

infrasity-learning

Infrasity Learning

Posted on May 23, 2024

Building Powerful React Components with Custom Hooks

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

Demo of Image Gallery App

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:

  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

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

# To start the web app server
npm run dev
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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

@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;
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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

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;
Enter fullscreen mode Exit fullscreen mode

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

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;
};
Enter fullscreen mode Exit fullscreen mode

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

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;
Enter fullscreen mode Exit fullscreen mode

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

function ErrorMessage({ children }) {
  return (
    <div className="error">
      <h2>Error ⚠️</h2>
      <p>{children}</p>
    </div>
  );
}

export default ErrorMessage;
Enter fullscreen mode Exit fullscreen mode

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

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;
Enter fullscreen mode Exit fullscreen mode

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

Oh No! meme | Credit: Tenor

The Power of Separation: Enter Custom Hooks

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

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 };
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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 };
}
Enter fullscreen mode Exit fullscreen mode

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.

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
Enter fullscreen mode Exit fullscreen mode

Congratulations!!! πŸŽ‰

We just made our custom hook generalized and hence; more reusable!

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

πŸ’– πŸ’ͺ πŸ™… 🚩
infrasity-learning
Infrasity Learning

Posted on May 23, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related