Essential React Custom Hooks
Tags: javascript, programming, react, beginners
Published: 2024-02-20 10:10:43

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

```tsx
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;
}
```

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.

```tsx
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..."
    />
  );
}

```

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

```tsx
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];
};
```

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.

```jsx
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>
  );
}

```

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

```tsx
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;
};
```

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.

```jsx
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>
  );
}

```

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.