DEV Community

Cover image for Essential React Custom Hooks
Sheikh Ahnaf Hasan
Sheikh Ahnaf Hasan

Posted on

Essential React Custom Hooks

Hello there! Having spent several years developing React applications, I've noticed a handful of functionalities that pop up again and again. They're not just limited to a single project, but are versatile enough to be used in various contexts. Recognizing and transforming these recurring functionalities into custom hooks can save us a ton of time and effort. Plus, these custom hooks are super adaptable and can be easily integrated into almost any project, no matter how big or small, or how simple or complex.

Let's dig a little deeper into these useful custom hooks and chat about how they can be practically applied:

  • useDebounce: This handy hook lets us delay the processing of a function for a set amount of time. It's a lifesaver when we want to limit how often a function can trigger or run.
  • useLocalStorageState: With this hook, we can easily save, retrieve, and play with data in the browser's local storage. It's a gem for keeping the state consistent across sessions or tabs.
  • useWindowScroll: This nifty custom hook helps keep track of the window's scroll position. It's great for a range of uses like lazy loading images, infinite scrolling, or revealing/hiding elements based on the scroll position.

useDebounce

import { useEffect, useState } from "react";

export function useDebounce({
  value,
  delay,
}: {
  value: string;
  delay: number;
}) {
  // State and setters for debounced value
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(
    () => {
      // Update debounced value after delay
      const handler = setTimeout(() => {
        setDebouncedValue(value);
      }, delay);

      // Cancel the timeout if value changes (also on delay change or unmount)
      // This is how we prevent debounced value from updating if value is changed ...
      // .. within the delay period. Timeout gets cleared and restarted.
      return () => {
        clearTimeout(handler);
      };
    },
    [value, delay] // Only re-call effect if value or delay changes
  );

  return debouncedValue;
}
Enter fullscreen mode Exit fullscreen mode

The useDebounce is a custom React hook designed to delay the update of a state value until after a specified delay. This is useful for reducing the number of updates that occur in response to rapid or frequent changes to a value, such as user input in a text field. Let's break down how it works and provide an example of its usage.

How useDebounce Works:

  1. Input Parameters: It takes an object with two properties:
    • value: The current value that you want to debounce.
    • delay: The amount of time (in milliseconds) you want to wait after the last change before updating the debounced value.
  2. State: It initializes a state variable debouncedValue with the initial value passed to the hook.
  3. Effect Hook: It uses the useEffect hook to perform side effects:
    • Set Timeout: Inside useEffect, it sets a timeout that waits for the specified delay before updating debouncedValue to the current value.
    • Cleanup Function: Returns a cleanup function that clears the timeout if the value or delay changes before the timeout completes. This prevents the debounced value from updating if the input value changes again within the delay period.
  4. Return: It returns the debouncedValue. This value reflects the latest value passed to the hook, but only after the specified delay has elapsed without any further changes to the value.

Example Usage:

Imagine you have a search input field where you want to fetch search results from an API, but you want to minimize the number of API calls by only fetching the results once the user has stopped typing for 500 milliseconds.

import React, { useState } from 'react';
import { useDebounce } from './useDebounce';

function SearchComponent() {
  const [inputValue, setInputValue] = useState('');
  const debouncedSearchTerm = useDebounce({ value: inputValue, delay: 500 });

  // Effect for API call
  useEffect(() => {
    if (debouncedSearchTerm) {
      // Function to fetch search results
      console.log(`Fetching search results for: ${debouncedSearchTerm}`);
      // Here you would fetch your search results using debouncedSearchTerm
    }
  }, [debouncedSearchTerm]); // This effect runs only when debouncedSearchTerm changes

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

Enter fullscreen mode Exit fullscreen mode

In this example:

  • The user types into the search input field.
  • The inputValue state updates with each keystroke, but the debouncedSearchTerm only updates after the user has stopped typing for 500 milliseconds.
  • The useEffect hook listening to debouncedSearchTerm then triggers, potentially fetching search results based on the debounced term.

This approach ensures that your application performs the search operation efficiently, reducing unnecessary API calls or processing when the user is typing fast.

useLocalStorageState

import { useState, useEffect, useMemo } from "react";

// Define a custom hook that syncs state with local storage
export const useLocalStorageState = <T>(
  key: string,
  initialValue: T
): [T, React.Dispatch<React.SetStateAction<T>>] => {
  // Get the initial value from local storage, if available
  const storedValue =
    typeof window !== "undefined" ? localStorage.getItem(key) : null;

  const initial = useMemo(() => {
    try {
      return storedValue ? JSON.parse(storedValue) : initialValue;
    } catch (error) {}
  }, [initialValue, storedValue]);

  // Create the state using useState
  const [state, setState] = useState<T>(initial);

  // Update local storage whenever the state changes
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(state));
  }, [key, state]);

  return [state, setState];
};

// Path: src/lib/hooks/use-sessionstorage-state.hook.ts
export const useSessionStorageState = <T>(
  key: string,
  initialValue: T
): [T, React.Dispatch<React.SetStateAction<T>>] => {
  const storedValue =
    typeof window !== "undefined" ? sessionStorage.getItem(key) : null;
  const initial = useMemo(() => {
    try {
      return storedValue ? JSON.parse(storedValue) : initialValue;
    } catch (error) {}
  }, [initialValue, storedValue]);
  const [state, setState] = useState<T>(initial);
  useEffect(() => {
    sessionStorage.setItem(key, JSON.stringify(state));
  }, [key, state]);
  return [state, setState];
};
Enter fullscreen mode Exit fullscreen mode

The useLocalStorageState function is a custom React hook designed to synchronize a component's state with the local storage of the browser. This allows the state to persist across browser sessions. Here's a breakdown of how it works and an example of how to use it.

How useLocalStorageState Works:

  1. Generics: It uses TypeScript generics (<T>) to allow for flexibility in the type of value it can handle (e.g., string, number, object).
  2. Input Parameters:
    • key: A unique string that identifies the item in local storage.
    • initialValue: The initial value for the state if no value is found in local storage.
  3. Retrieving Initial Value:
    • Checks if the window object is available to ensure code doesn't break during server-side rendering (e.g., in Next.js applications).
    • Attempts to get the stored value from local storage using the provided key.
    • Uses useMemo to parse the stored JSON value only once or fallback to the initialValue if parsing fails or if no value is found.
  4. State Management:
    • Initializes the state with either the parsed local storage value or the provided initial value.
  5. Effect for Syncing with Local Storage:
    • Utilizes useEffect to update the local storage item whenever the state changes. It serializes the state to a JSON string and stores it under the provided key.
  6. Return Values:
    • Returns a tuple containing the current state and a setter function (setState), similar to the return value of the useState hook.

Example Usage:

Let's say you have a form input where you want to persist the user's input even if they refresh the page or close and reopen the browser.

import React from 'react';
import { useLocalStorageState } from './useLocalStorageState';

function FormComponent() {
  const [name, setName] = useLocalStorageState('userName', '');

  return (
    <div>
      <label htmlFor="name">Name:</label>
      <input
        id="name"
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <p>Hello, {name}!</p>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

In this example:

  • The useLocalStorageState hook is used to bind the input field's value to a piece of state that is synchronized with local storage. The key is 'userName'.
  • If the user enters their name and then refreshes the page, the input field will still display their name because it was saved to local storage.
  • The state is initially set to the value retrieved from local storage if it exists; otherwise, it falls back to an empty string ('').
  • Any changes to the input field update both the state and the corresponding item in local storage, ensuring the two are always in sync.

This approach enhances the user experience by making the UI state persistent across sessions without requiring any backend infrastructure to store user data.

useWindowScroll

import { useEffect, useState } from "react";

interface WindowScrollState {
  x: number;
  y: number;
}

export const useWindowScroll = (): WindowScrollState => {
  const [scrollPosition, setScrollPosition] = useState<WindowScrollState>({
    x: typeof window !== "undefined" ? window.scrollX : 0,
    y: typeof window !== "undefined" ? window.scrollY : 0,
  });

  useEffect(() => {
    const handleScroll = () => {
      setScrollPosition({
        x: typeof window !== "undefined" ? window.scrollX : 0,
        y: typeof window !== "undefined" ? window.scrollY : 0,
      });
    };

    if (typeof window !== "undefined") {
      // Attach the scroll event listener when the component mounts
      window.addEventListener("scroll", handleScroll);

      // Remove the scroll event listener when the component unmounts
      return () => {
        window.removeEventListener("scroll", handleScroll);
      };
    }
  }, []);

  return scrollPosition;
};
Enter fullscreen mode Exit fullscreen mode

The useWindowScroll function is a custom React hook designed to track and provide the window's scroll position. It encapsulates the logic for listening to scroll events and updating the state accordingly to reflect the current scroll position of the window. This can be particularly useful for features like showing or hiding elements based on scroll position, implementing scroll-based animations, or simply monitoring user scroll behavior. Let's break down its workings and illustrate its usage with an example.

How useWindowScroll Works:

  1. State Initialization: Initializes a state variable scrollPosition with an object that contains two properties:
    • x: Represents the horizontal scroll position. It's set to window.scrollX if the window object is available, otherwise to 0.
    • y: Represents the vertical scroll position. It's set to window.scrollY if the window object is available, otherwise to 0.
  2. Effect Hook: Uses useEffect to set up side effects:
    • Scroll Event Listener: Defines a function handleScroll that updates scrollPosition based on the current scroll position of the window.
    • Event Listener Registration: If the window object is available, it adds handleScroll as an event listener to the window's scroll event. This ensures the scroll position is updated whenever the user scrolls.
    • Cleanup: Returns a cleanup function that removes the scroll event listener when the component using this hook unmounts, preventing potential memory leaks.
  3. Return Value: Returns the current scrollPosition state, allowing components to access the latest scroll position as { x, y }.

Example Usage:

Consider a scenario where you want to display a "Back to Top" button only when the user has scrolled down a significant amount.

import React from 'react';
import { useWindowScroll } from './useWindowScroll';

function BackToTopButton() {
  const { y } = useWindowScroll();

  const handleBackToTop = () => {
    window.scrollTo({ top: 0, behavior: 'smooth' });
  };

  return (
    <button
      style={{
        position: 'fixed',
        bottom: '20px',
        right: '20px',
        display: y > 200 ? 'block' : 'none',
      }}
      onClick={handleBackToTop}
    >
      Back to Top
    </button>
  );
}

Enter fullscreen mode Exit fullscreen mode

In this example:

  • The useWindowScroll hook provides the vertical scroll position (y) of the window.
  • The "Back to Top" button is conditionally rendered based on the scroll position. It appears only when the user has scrolled down more than 200 pixels.
  • Clicking the button scrolls the window back to the top smoothly.

This approach enhances user navigation on long pages by providing an easily accessible means to return to the top of the page, improving the overall user experience.

Top comments (0)