Custom React Hooks

Custom React Hooks

So, you know those neat React custom hooks? They're like your code's best friends. These little gems let you whip up some super reusable logic for your functional components. Imagine breaking down your components into these neat, bite-sized pieces, each doing exactly what they're meant to do. And the best part? Testing and understanding become a breeze! You can isolate and test the logic all on its own, without dragging the whole component circus into the act. Plus, if you're all about teamwork, custom hooks are your allies for sharing logic between different components. No more copy-pasting madness – just clean, maintainable, and updated codebase goodness. Here are five custom hooks to streamline your development:

1. useToggle

typescript
import { useState } from "react"; export default function useToggle( defaultValue: boolean ): [boolean, (value?: boolean) => void] { const [value, setValue] = useState<boolean>(defaultValue); function toggleValue(value?: boolean) { setValue(currentValue => (value !== undefined ? value : !currentValue)); } return [value, toggleValue]; }

This simple hook simplifies a component toggling between boolean values. It's designed to manage a boolean value and provide a method to toggle that value. The hook takes an initial defaultValue of type boolean, and returns an array containing the current value and a function to toggle it.

Breakdown of how it works:

  1. Initialization: The hook initializes the state with the provided defaultValue.
  2. Value and Toggle Function: The hook returns an array where the first element is the current boolean value (value), and the second element is a function (toggleValue) to toggle the value.
  3. Toggle Logic: When the toggleValue function is called, it accepts an optional boolean parameter (value). If a value is provided, the boolean value is set to that value. If no value is provided, the current boolean value is toggled (flipped from true to false or vice versa).

Example usage:

tsx
import React from "react"; import useToggle from "./useToggle"; // Assuming the hook is in a file named useToggle.js function ToggleExample() { const [isToggled, toggleIsToggled] = useToggle(false); return ( <div> <button onClick={toggleIsToggled}>Toggle</button> <p>Status: {isToggled ? "On" : "Off"}</p> </div> ); }

In this example, the ToggleExample component uses the useToggle hook to manage the state of a toggle button. The isToggled value holds the current state, and the toggleIsToggled function is used to toggle the value when the button is clicked. This custom hook streamlines the process of managing and toggling boolean states in your React components.

2. useTimeout

typescript
import { useCallback, useEffect, useRef } from "react"; type TimeoutController = { reset: () => void; clear: () => void; }; export default function useTimeout( callback: () => void, delay: number ): TimeoutController { const callbackRef = useRef<() => void>(callback); const timeoutRef = useRef<NodeJS.Timeout | undefined>(); useEffect(() => { callbackRef.current = callback; }, [callback]); const set = useCallback(() => { timeoutRef.current = setTimeout(() => callbackRef.current(), delay); }, [delay]); const clear = useCallback(() => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }, []); useEffect(() => { set(); return clear; }, [delay, set, clear]); const reset = useCallback(() => { clear(); set(); }, [clear, set]); return { reset, clear }; }

This custom useTimeout hook provides a way to manage timeouts in React components. It accepts a callback function and a delay in milliseconds. The hook sets up a mechanism to run the provided callback after the specified delay, allowing you to control and manage timeouts in your components.

Breakdown of how it works:

  1. Initialization:
    • The hook takes in two parameters: callback, which is the function you want to execute after the specified delay, and delay, which is the time in milliseconds before the callback is executed.
  2. References:
    • callbackRef is a useRef that keeps track of the latest version of the callback function. It's used to ensure the most up-to-date callback is executed, even if the component re-renders.
    • timeoutRef is another useRef that keeps track of the timeout instance returned by setTimeout.
  3. Effect to Update Callback Reference:
    • An effect runs whenever the callback changes. It updates callbackRef to store the latest callback function.
  4. Set Timeout:
    • The set function is a useCallback that sets up a new timeout using the latest version of the callback and the specified delay.
    • When the set function is called, it clears the previous timeout (if any) and sets a new one with the updated callback.
  5. Clear Timeout:
    • The clear function is another useCallback that cancels the current timeout (if it exists) using clearTimeout.
  6. Effect to Set Timeout and Cleanup:
    • Another effect runs when delay, set, or clear functions change. This effect is responsible for:
      • Setting up the initial timeout using the set function
      • Returning the clear function to clean up the timeout when the component unmounts or when the effect dependencies change.
  7. Reset Function:
    • The reset function is a useCallback that clears the existing timeout using clear and then sets up a new timeout using set.
    • This function can be used to delay the execution of the callback by resetting the timeout.
  8. Return Value:
    • The hook returns an object containing two functions: reset and clear.
    • reset can be used to delay the execution of the callback.
    • clear can be used to cancel the pending timeout.

Example usage:

Let's say you're building a notification system that automatically dismisses messages after a certain time. Here's how you might use this useTimeout hook for this:

tsx
import React from "react"; import useTimeout from "./useTimeout"; // Assuming the hook is in a file named useTimeout.js function Notification({ message, onClose }) { const { reset } = useTimeout(onClose, 5000); return ( <div className="notification"> <p>{message}</p> <button onClick={onClose}>Dismiss</button> <button onClick={reset}>Snooze</button> </div> ); }

In this example, the Notification component uses the useTimeout hook to automatically close the notification after 5 seconds (5000 milliseconds). The reset function from the hook can be used to delay dismissal by resetting the timeout. By encapsulating timeout management logic within this custom hook, you simplify the process of setting up and managing timeouts in your components. This promotes cleaner code and better separation of concerns when handling delayed actions in your application.

3. useDebounce

typescript
import { useEffect, DependencyList } from "react"; import useTimeout from "useTimeout"; // Path to the useTimeout hook type DebounceHook = { reset: () => void; clear: () => void; }; export default function useDebounce( callback: () => void, delay: number, dependencies: DependencyList ): void { const { reset, clear } = useTimeout(callback, delay); useEffect(reset, [...dependencies, reset]); useEffect(clear, []); }

The useDebounce hook allows components to momentarily postpone the execution of a callback function, granting control over when it's called. By making clever use of both the built-in useEffect hook provided by React and useTimeout hook above, the useDebounce hook equips you to effectively manage callback timing in your components.

Breakdown of how it works:

  1. Parmeters:
    • callback: This is the function you want to delay the execution of.
    • delay: The amount of time you want to wait before executing the callback.
    • dependencies: These are the dependencies from your component that, when changed, will trigger the debounced callback.
  2. useTimeout Hook:
    • The useTimeout hook described above is used to manage the timing aspect of debouncing.
  3. reset and clear Functions:
    • The reset function is part of the useTimeout hook. It resets the timeout so that the callback isn't executed immediately.
    • The clear function is also part of the useTimeout hook. It cancels the pending timeout of needed.
  4. First useEffect Hook:
    • This effect runs when the dependencies or the reset function changes.
    • It is responsible for resetting the timeout whenever the dependencies change of the reset function is called. This effectively delays the execution of the callback.
  5. Second useEffect Hook:
    • This effect runs when the component unmounts.
    • It's responsible for cancelling the pending timeout to ensure no delayed execution occurs after the component is no longer in use.

Example usage:

Imagine you're building a search feature that sends an API request when a user types in a search query. You want to debounce the API calls to avoid making too many requests in a short amount of time. Here's how you could use the useDebounce hook for this:

tsx
import React, { useState } from "react"; import useDebounce from "./useDebounce"; // Assuming the hook is in a file named useDebounce.js function SearchBar() { const [searchQuery, setSearchQuery] = useState(""); // Debounced API call const debouncedApiCall = useDebounce(() => { // Perform API call using searchQuery console.log("API call for:", searchQuery); }, 500, [searchQuery]); const handleInputChange = event => { setSearchQuery(event.target.value); debouncedApiCall(); // This will be executed after a delay of 500ms after the user stops typing. }; return ( <input type="text" value={searchQuery} onChange={handleInputChange} placeholder="Search..." /> ); }

In this example, the SearchBar component uses the useDebounce hook to ensure that the API call is triggered only after the user stops typing for 500 milliseconds. This helps optimize the use of the resources and prevents unnecessary API requests while providing a responsive search experience.

4. useStorage

typescript
import { useCallback, useState, useEffect } from "react"; export function useLocalStorage<T>( key: string, defaultValue: T | (() => T) ): [T | undefined, React.Dispatch<React.SetStateAction<T | undefined>>, () => void] { return useStorage(key, defaultValue, window.localStorage); } export function useSessionStorage<T>( key: string, defaultValue: T | (() => T) ): [T | undefined, React.Dispatch<React.SetStateAction<T | undefined>>, () => void] { return useStorage(key, defaultValue, window.sessionStorage); } function useStorage<T>( key: string, defaultValue: T | (() => T), storageObject: Storage ): [T | undefined, React.Dispatch<React.SetStateAction<T | undefined>>, () => void] { const [value, setValue] = useState<T | undefined>(() => { const jsonValue = storageObject.getItem(key); if (jsonValue != null) return JSON.parse(jsonValue); if (typeof defaultValue === "function") { return defaultValue(); } else { return defaultValue; } }); useEffect(() => { if (value === undefined) return storageObject.removeItem(key); storageObject.setItem(key, JSON.stringify(value)); }, [key, value, storageObject]); const remove = useCallback(() => { setValue(undefined); }, []); return [value, setValue, remove]; }

The useLocalStorage and useSessionStorage custom React hooks offer a streamlined way for components to securely store a piece of information in either the browser's LocalStorage or SessionStorage. These hooks ensure that the stored value stays in harmony with the component's internal state, allowing you to effortlessly manage persistence.

These hooks make use of the built-in useState and useEffect hooks provided by the React library, along with the useCallback hook for optimization.

Both the useLocalStorage and useSessionStorage functions operate in a similar manner but make use of different storage mechanisms: localStorage and sessionStorage, respectively. They accept two essential parameters: key and defaultValue. The key acts as the identifier for the stored value within the storage object, while the defaultValue is the fallback value that comes into play when the specified key is not found in the storage object.

Breakdown of how it works:

  1. Parameters:
    • key: This parameter is used as an identifier for the value within the storage object.
    • defaultValue: This parameter is either a default value or a function that returns the default value.
    • storageObject: This parameter determines whether to use localStorage or sessionStorage.
  2. Initialization of State:
    • The value state variable is initialized using the useState hook.
    • The defaultValue is used to set the initial value, either directly or by invoking the provided function if it's a function.
  3. Storage Retrieval:
    • Inside the useState initialization, the stored value is retrieved from the storage object using the provided key.
    • If the stored JSON value exists (jsonValue is not null), it is parsed and set as the initial value.
    • If the stored value is not found, the defaultValue is used (either directly or by invoking the function).
  4. Update and Persistence Effect:
    • An useEffect runs whenever the value changes, as well as when the key changes.
    • If the value becomes undefined, indicating removal, the corresponding item in the storage is removed using removeItem.
    • If the value is not undefined, it's stored in the storage as a JSON string using setItem.
  5. Removal Callback:
    • The remove function is created using useCallback.
    • This function sets the value to undefined, which in turn triggers the useEffect to remove the stored item.
  6. Return Value:
    • The hook returns an array containing three elements:
      • value: The current stored value or the default value.
      • setValue: A function to update the stored value.
      • remove: A function to remove a stored value.

Example usage:

tsx
import React from "react"; import { useSessionStorage, useLocalStorage } from "./useStorage"; function StorageComponent() { const [name, setName, removeName] = useSessionStorage<string>("name", "Kyle"); const [age, setAge, removeAge] = useLocalStorage<number>("age", 26); return ( <div> <div> {name} - {age} </div> <button onClick={() => setName("John")}>Set Name</button> <button onClick={() => setAge(40)}>Set Age</button> <button onClick={removeName}>Remove Name</button> <button onClick={removeAge}>Remove Age</button> </div> ); }

In this example, the StorageComponent is utilizing the useSessionStorage and useLocalStorage hooks from the useStorage module to manage data persistence. Let's dissect this one:

  1. The useSessionStorage and useLocalStorage hooks are imported from the useStorage module. These hooks give us a way to store data in the browsers's sessionStorage and localStorage respectively.
  2. Inside the StorageComponent, two sets of state variables (name, setName, removeName, age, setAge, removeAge) are declared using the custom hooks. These state variables will be used to manage the stored values and interactions with those values.
  3. The name state variable is connected to useSessionStorage, using a string type. The default value is set to "Kyle".
  4. The age state variable is connected to useLocalStorage, using a number type. The default value is set to 26.
  5. The component's rendering includes:
    • A div that displays the current name and age values.
    • Four buttons:
      • "Set Name" button, which sets the name to "John".
      • "Set Age" button, which sets the age to 40.
      • "Remove Name" button, which triggers the removal of the stored name.
      • "Remove Age" button, which triggers the removal of the stored age.

By utilizing these hooks, the component manages the storage and interaction with the stored data seamlessly. The hooks abstract away the details of working with localStorage and sessionStorage, making it easier to integrate persistent data management into React components.

5. useArray

tsx
import { useState } from "react"; type ArrayActions<T> = { push: (element: T) => void; filter: (callback: (element: T) => boolean) => void; update: (index: number, newElement: T) => void; remove: (index: number) => void; clear: () => void; }; export default function useArray<T>(defaultValue: T[]): { array: T[]; } & ArrayActions<T> { const [array, setArray] = useState<T[]>(defaultValue); const push = (element: T) => { setArray(a => [...a, element]); }; const filter = (callback: (element: T) => boolean) => { setArray(a => a.filter(callback)); }; const update = (index: number, newElement: T) => { setArray(a => [ ...a.slice(0, index), newElement, ...a.slice(index + 1, a.length), ]); }; const remove = (index: number) => { setArray(a => [...a.slice(0, index), ...a.slice(index + 1, a.length)]); }; const clear = () => { setArray([]); }; return { array, push, filter, update, remove, clear }; }

The useArray hook is designed to aid components in managing their array-based state. The foundation of this hook rests upon the inherent useState hook from the React library. By passing in the defaultValue parameter, the array state is gracefully initiated, enabling seamless integration of array-related functionality.

Breakdown of how it works:

  1. Parameters and State Initialization:
    • The useArray hook takes a defaultValue array as input.
    • The array state variable is initialized with the provided defaultValue.
  2. Array Manipulation Functions:
    • The hook defines several functions for manipulating the array:
      • push: Appends an element to the end of the array.
      • filter: Filters the array based on the provided callback.
      • update: Updates an element at a specific index.
      • remove: Removes an element at a specific index.
      • clear: Clears the entire array.
  3. Updating the Array:
    • Each function that manipulates the array uses the setArray function provided by the useState hook to update the state.

Example usage:

tsx
import React from "react"; import useArray from "./useArray"; // Assuming the hook is in a file named useArray.ts function ArrayExample() { const { array, push, filter, update, remove, clear } = useArray<number>([1, 2, 3]); return ( <div> <p>Array: {array.join(", ")}</p> <button onClick={() => push(4)}>Add 4</button> <button onClick={() => filter(element => element % 2 === 0)}>Filter Even</button> <button onClick={() => update(1, 5)}>Update at Index 1</button> <button onClick={() => remove(2)}>Remove at Index 2</button> <button onClick={clear}>Clear Array</button> </div> ); }

In this example, the ArrayExample component utilizes the useArray hook to manage an array of numbers. The rendered output displays the current state of the array. The buttons allow you to manipulate the array using the defined functions: push, filter, update, remove, and clear. Clicking on these buttons triggers the corresponding array manipulation action, which in turn updates the component's state and re-renders the array. This hook makes it convenient to manage and manipulate arrays within your components.

Until next time...

Wrapping up, custom hooks can be your secret weapon for building projects that rock - they're like magic, even when things get mega-sized. So go ahead and toss them into your code whenever you need that extra oomph. Thanks a bunch for hanging in there with this article. I get it, it was a bit of a marathon, but I hope you found it worthwhile!