Building Smarter React Apps with Custom Hook

Building Smarter React Apps with Custom Hook

React Hooks revolutionized state management and side effects, making it easier to reuse logic without class components. While built-in hooks like useState, useEffect, and useContext are powerful, Custom Hooks allow developers to encapsulate complex logic and reuse it across multiple components.

In this article, we’ll explore advanced Custom Hook use cases such as efficiently fetching API data, managing authentication, integrating localStorage, implementing global event listeners, and optimizing performance with debouncing and throttling.

Understanding Custom Hooks

Custom Hooks in React allow developers to extract and reuse stateful logic across multiple components. They follow the same rules as built-in hooks like useState and useEffect, but provide a way to encapsulate complex functionality in a clean, modular, and reusable manner.

React introduced Hooks in version 16.8 to move away from class components and simplify state and side effect management in functional components. While built-in hooks handle common functionalities, real-world applications often require custom logic that needs to be used in multiple places. Instead of duplicating code across components, Custom Hooks help in maintaining cleaner and more maintainable codebases.

Why Use Custom Hooks?

  • Code Reusability – Instead of writing the same logic in multiple components, a Custom Hook allows you to encapsulate it once and use it anywhere.

  • Separation of Concerns – Keeps component logic clean and focused on rendering UI, while hooks handle side effects and state management.

  • Improved Readability and Maintainability – By moving complex logic into Custom Hooks, components become smaller, easier to read, and more maintainable.

  • Encapsulation of Side Effects – Custom Hooks allow better management of side effects like API calls, event listeners, and timers without cluttering component code.

  • Testing and Debugging – Hooks can be tested independently, making debugging easier and reducing unexpected behavior in UI components.

Example: A Basic Custom Hook

import { useState } from "react";

function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);

  const increment = () => setCount(prevCount => prevCount + 1);
  const decrement = () => setCount(prevCount => prevCount - 1);
  const reset = () => setCount(initialValue);

  return { count, increment, decrement, reset };
}

export default useCounter;

Here, we have a useCounter hook that handles counter state in React. It starts with a given value and offers functions to increase, decrease, and reset the count.

Now, we can use it in any component:

const CounterComponent = () => {
  const { count, increment, decrement, reset } = useCounter(10);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
};

Custom Hook for Fetching API Data

Fetching data is one of the most common tasks in React apps. A Custom Hook can encapsulate API logic and avoid repetitive code.

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 () => {
      try {
        const response = await fetch(url);
        if (!response.ok){
            throw new Error("Failed to fetch data");
        }

        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url]);

  return { data, loading, error };
}

export default useFetch;

Using the useFetch Hook

const Posts = () => {
  const { data, loading, error } = useFetch("https://jsonplaceholder.typicode.com/posts");

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;

  return (
    <ul>
      {data.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
};

Custom Hook for Managing LocalStorage

Managing localStorage in React can be simplified using a Custom Hook:

import { useState, useEffect } from "react";

function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const storedValue = localStorage.getItem(key);
    return storedValue ? JSON.parse(storedValue) : initialValue;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}

export default useLocalStorage;

Here, useLocalStorage custom hook manages state while syncing it with localStorage. It initializes the state with a stored value (if available) or a provided default. Whenever the state updates, it automatically saves the new value to localStorage, ensuring data persistence across page reloads.

Using the useLocalStorage Hook

const ThemeSwitcher = () => {
  const [theme, setTheme] = useLocalStorage("theme", "light");

  return (
    <div>
      <p>Current Theme: {theme}</p>
      <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
        Toggle Theme
      </button>
    </div>
  );
};

Custom Hook for Debouncing User Input

Debouncing is useful to prevent excessive API calls when a user types in a search box.

import { useState, useEffect } from "react";

function useDebounce(value, delay = 500) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => clearTimeout(handler);
  }, [value, delay]);

  return debouncedValue;
}

export default useDebounce;

Here, useDebounce custom hook delays updating the state until after a specified time (delay) has passed since the last change to value. It uses setTimeout inside useEffect to update debouncedValue, clearing the previous timeout on each re-render. This is useful for optimizing performance in scenarios like search input handling.

Using the useDebounce Hook

const SearchBox = () => {
  const [search, setSearch] = useState("");
  const debouncedSearch = useDebounce(search, 500);

  useEffect(() => {
    if (debouncedSearch) {
      console.log("Fetching results for:", debouncedSearch);
      // Call API with debouncedSearch
    }
  }, [debouncedSearch]);

  return <input type="text" value={search} onChange={(e) => setSearch(e.target.value)} placeholder="Search..." />;
};

Conclusion

Custom Hooks provide an effective way to encapsulate and reuse logic in React applications, improving both modularity and performance. By leveraging hooks for tasks such as data fetching, authentication, localStorage management, and optimizing user interactions through debouncing or throttling, developers can maintain cleaner, more efficient, and scalable code. Error Boundaries help handle component-level errors, while hooks like useFetch, useLocalStorage, and useDebounce streamline common functionalities, reducing redundancy and enhancing the overall user experience. Implementing Custom Hooks allows for better separation of concerns, making applications more maintainable and easier to debug.