DEV Community

Victor Ogbonna
Victor Ogbonna

Posted on

Creating Custom Hooks for Reusable Logic in React

In simple terms, React hooks are functions that let you make use of React state and lifecycle features from function components. They let you use React without classes which is why they don’t work inside classes, these functions are used to manage state in a functional component.
Which is why if you’re trying to reuse logic across components, Custom Hooks should be your go-to. They are a very powerful tool, because they give you the ability to create hooks for very specific purposes like, to fetch data, or to keep track of a user’s online presence.
In this article, we explore everything about them from their benefits, common use cases, the best ways to create them, and lots more.

What are Custom Hooks?
Custom hooks in react are reusable functions that are used to extract component logic that needs to be used by multiple components, hence why they are called reusable functions. They are used by most developers to add special and unique functionality to their react applications.
The basic structure of a custom hook isn’t much different from other react hooks, it starts with the word “use”. They follow the same rules as built-in hooks because they have to be used in the same fashion. You can check out these rules here; Rules of Hooks – React.

Benefits of Using Custom Hooks
Custom hooks in React simplify code by increasing its modularity, organisation, and maintainability. Custom hooks keep components focused on rendering by removing redundant code and streamlining complex functionality by extracting and reusing common logic. Cleaner, more manageable code that grows with applications is the result of this method. These benefits include;

  1. Code Reusability: You may extract and reuse common functionality across several components with the help of custom hooks. This keeps components small and rendering-focused, while custom hooks manage shared features like form validation or API calls.
  2. Cleaner, More Readable Code: Components become clearer and more concise when logic is isolated in custom hooks. This division clarifies the functions of each component (such as rendering) and how they are carried out (logic managed via hooks).
  3. Improved Maintainability: You can update sophisticated logic in a single location if necessary by isolating it in custom hooks. This makes debugging easier and lowers the possibility of discrepancies.
  4. Better State Management: You can control side effects and local state in one location with custom hooks. Particularly in larger systems, this modular architecture facilitates state tracking and handling.
  5. Encapsulating Side Effects: Side effects like data retrieval and event subscription can be controlled by custom hooks. The constant application of the same behaviour across several components is guaranteed by this encapsulation.
  6. Enhanced Testing: You can test functionality without relying on the user interface by isolating logic behind custom hooks. This modular approach increases overall application reliability and facilitates the testing of certain behaviours.
  7. DRY Principle Compliance: The "Don't Repeat Yourself" (DRY) philosophy, which reduces code duplication by sharing common capabilities across components, is easier to follow when you use custom hooks.
  8. Flexible Abstraction Layer: You can abstract away lower-level details with custom hooks. To manage data fetching, for example, you can write a useFetch hook that can be used again, independent of the data source, which makes the code flexible and scalable.

When to Create a Custom Hook
Creating a custom hook in React is useful in the following scenarios:

  • Repeating logic in multiple components:To keep your code DRY (Don't Repeat Yourself), a custom hook can centralise logic that you find yourself repeating across components, such as controlling input fields, toggling items, or completing calculations.
  • Handling complex or stateful processes:Custom hooks can encapsulate sophisticated state management for activities like data fetching, form validation, pagination, or storing data in local storage. By isolating concerns, these hooks make components clearer and easier to follow.
  • Abstracting side effects or data transformations: Custom hooks are perfect for handling side effects or data transformation, particularly when these operations like API requests, data filtering, or formatting responses—need independent testing or will be used repeatedly. Additionally, it enables you to handle faults or edge cases uniformly across components.

Example 1: Data Fetching Hook
Here's a walkthrough of a basic useFetch custom hook in React, designed to simplify data fetching from APIs or databases.
Structure and Explanation

  1. Accepting URL as a Parameter: The hook takes a URL parameter to determine where to fetch data from, making it reusable for different endpoints.
  2. Using useState for State Management: The hook uses useState to store fetched data, a loading state, and any error encountered during fetching.
  3. Using useEffect for Side Effects: The hook employs useEffect to fetch data when the component mounts or when the URL changes. Code Example
import { useState, useEffect } from 'react';

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);
      try {
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error(`Error: ${response.status}`);
        }
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url]);

  return { data, loading, error };
}

export default useFetch;
Enter fullscreen mode Exit fullscreen mode

Explanation of Each Part
useState Declarations:

  • data: Holds the fetched data once it's retrieved.
  • loading: Indicates if the data is still being fetched, starting as true.
  • error: Stores any error message encountered during fetching.

useEffect Hook:

  • Dependency Array ([url]): The hook runs whenever the URL changes, allowing you to use it with different endpoints.
  • fetchData Function: An asynchronous function that performs the fetch request.

  • setLoading(true): Sets the loading state to true before fetching starts.

  • fetch(url): Requests data from the provided URL.

  • Error Handling: Throws an error if the response isn’t ok (e.g., 404 or 500 status).

  • setData(result): Sets the retrieved data.

  • setError(err.message): Updates the error state in case of failure.

  • setLoading(false): Turns off the loading indicator once fetching completes, regardless of success or failure.

Returning State: The hook returns an object with data, loading, and error, which the component can use to display loading states, handle errors, and render data.

Use Cases
The useFetch hook is useful in scenarios such as:

  • Fetching data from REST APIs: For instance, retrieving user information, posts, or product listings from an API.
  • Loading data from a database: You could fetch data from a backend server to display in your frontend UI.
  • Displaying dynamic content: Use useFetch to fetch and display content that changes frequently or depends on user input (like a search result).

This useFetch hook provides a clean and reusable way to handle data fetching in React applications, reducing boilerplate code and keeping components focused on rendering.

Example 2: Form Handling Hook
A useForm custom hook is helpful for managing form state, handling validation, and resetting form data in React applications. It allows you to streamline the process of capturing user input, managing form errors, and maintaining a clean component structure.
Structure and Explanation

  1. Form State Management: The hook initializes and tracks form input values using useState.
  2. Validation: Accepts a validation function to validate input values and track errors.
  3. Reset Function: Provides a reset function to clear the form state after submission or when needed. Code Example
import { useState } from 'react';

function useForm(initialValues, validate) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});

  const handleChange = (event) => {
    const { name, value } = event.target;
    setValues({
      ...values,
      [name]: value,
    });

    if (validate) {
      const error = validate(name, value);
      setErrors({
        ...errors,
        [name]: error,
      });
    }
  };

  const handleSubmit = (submitCallback) => (event) => {
    event.preventDefault();
    const validationErrors = validate ? validate(values) : {};
    setErrors(validationErrors);

    if (Object.keys(validationErrors).length === 0) {
      submitCallback();
    }
  };

  const resetForm = () => {
    setValues(initialValues);
    setErrors({});
  };

  return {
    values,
    errors,
    handleChange,
    handleSubmit,
    resetForm,
  };
}

export default useForm;
Enter fullscreen mode Exit fullscreen mode

Explanation of Each Part

useState Declarations:

a. values: Stores the form’s input values, initialized with initialValues passed into the hook.

b. errors: Stores validation error messages for each input field.

handleChange Function:

a. Extracts the name and value of the input field from the event, then updates the corresponding value in values.

b. If a validate function is provided, it checks the validity of the input and updates the errors state.

handleSubmit Function:

a. Prevents the default form submission event.

b. Runs the validation function (if available) across all form values and updates errors with any validation errors.

c. If there are no validation errors, it calls the submitCallback to handle form submission.

resetForm Function:

a. Resets values to initialValues and clears errors. This is useful after form submission or when a user wants to reset the form fields.

Returning State and Handlers:

a. The hook returns values, errors, handleChange, handleSubmit, and resetForm, giving full control over form data, validation errors, and actions.

Use Cases
The useForm hook is beneficial in several scenarios:

  • Handling User Input in Complex Forms: This hook is useful for managing form state in complex forms across different components.
  • Centralized Validation Logic: You can pass a validation function into the hook, allowing reusable and centralized validation logic for different forms in your application.
  • Reusable Form Functionality: The hook’s flexibility makes it ideal for handling forms on various pages, like login, registration, or profile update forms, without duplicating code.

By using the useForm hook, you can efficiently manage form input, validation, and resetting, leading to cleaner, more organized, and reusable code in your React applications.

Additional Common Custom Hook Ideas
Here are a few other common custom hook ideas for enhancing React applications:

  • useDebounce: Limits the rate at which a function is executed. This is particularly useful for optimizing performance with inputs like search bars, where you may want to delay API calls until the user stops typing.
  • useLocalStorage: Allows you to persist state in local storage. This is useful for storing user preferences, theme settings, or other data that should persist across sessions without needing a backend.
  • usePrevious: Stores the previous value of a variable or state, which can be helpful in cases where you need to compare current and previous values, like tracking a user's last input or checking if a value has changed.
  • useToggle: Manages boolean state with a simple toggle function, ideal for toggling between true and false for features like modal visibility or dark mode.
  • useOnClickOutside: Detects clicks outside a specified element. Commonly used for closing dropdowns or modals when the user clicks outside of them.
  • useFetchOnScroll: Loads additional data as the user scrolls, perfect for implementing infinite scrolling in applications where users can view continuous lists (e.g., a news feed).

Each of these custom hooks simplifies common patterns, making your code more efficient and easier to maintain.

Best Practices for Writing Custom Hooks

  • Keep hooks focused on a single purpose: Design each hook to handle one specific task, like data fetching or managing form inputs. This makes hooks easier to understand, maintain, and reuse across components.
  • Make them flexible by accepting parameters: Allow hooks to accept parameters so they can adapt to different situations. For example, a useFetch hook might accept a URL as a parameter to handle various API requests, enhancing its versatility.
  • Use proper naming conventions for readability: Start hook names with “use” (e.g., useForm, useFetchData) to indicate they follow React’s hook conventions. Clear, descriptive names improve readability and signal the hook’s function at a glance.
  • Avoid side effects inside the hook: Keep the logic of your custom hook focused on returning state or behavior. If side effects (like fetching data or manipulating the DOM) are required, use useEffect inside the hook to separate concerns and avoid unwanted side effects.
  • Return minimal state and functions: Keep the API of your custom hook simple by returning only the necessary state and functions. This makes it easier for other developers to consume the hook and prevents unnecessary complexity.
  • Document your hooks: Include comments or documentation within your custom hooks to explain what they do and how to use them. This is especially important if the hook is complex or has specific requirements, ensuring that others (or your future self) can understand and use it effectively.

Conclusion
To sum up, custom hooks are a crucial component of React applications' reusable and maintainable logic. Through the encapsulation of intricate functions such as form handling, data retrieval, and other stateful operations, custom hooks encourage code modularity and minimize component redundancy. They help you adhere to the DRY principle, enhance readability, and facilitate testing and maintenance of your code.

Additionally, React developers may simplify state management, isolate and control side effects, and build a more adaptable and scalable codebase with the help of custom hooks. When utilized properly, they facilitate the encapsulation and sharing of functionality, offering a strong abstraction layer that improves application performance and development efficiency. As you continue to develop and improve custom hooks, you can make sure they stay useful, efficient, and easy to use across all of your projects by keeping them focused on a single goal, adhering to clear naming conventions, and documenting their behavior.

Top comments (0)