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
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:
- Initialization: The hook initializes the state with the provided
defaultValue
. - 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. - Toggle Logic: When the
toggleValue
function is called, it accepts an optional boolean parameter (value
). If avalue
is provided, the boolean value is set to thatvalue
. If novalue
is provided, the current boolean value is toggled (flipped fromtrue
tofalse
or vice versa).
Example usage:
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
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:
- Initialization:
- The hook takes in two parameters:
callback
, which is the function you want to execute after the specified delay, anddelay
, which is the time in milliseconds before the callback is executed.
- The hook takes in two parameters:
- References:
callbackRef
is auseRef
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 anotheruseRef
that keeps track of the timeout instance returned bysetTimeout
.
- Effect to Update Callback Reference:
- An effect runs whenever the
callback
changes. It updatescallbackRef
to store the latest callback function.
- An effect runs whenever the
- Set Timeout:
- The
set
function is auseCallback
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.
- The
- Clear Timeout:
- The
clear
function is anotheruseCallback
that cancels the current timeout (if it exists) usingclearTimeout
.
- The
- Effect to Set Timeout and Cleanup:
- Another effect runs when
delay
,set
, orclear
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.
- Setting up the initial timeout using the
- Another effect runs when
- Reset Function:
- The
reset
function is auseCallback
that clears the existing timeout usingclear
and then sets up a new timeout usingset
. - This function can be used to delay the execution of the callback by resetting the timeout.
- The
- Return Value:
- The hook returns an object containing two functions:
reset
andclear
. reset
can be used to delay the execution of the callback.clear
can be used to cancel the pending timeout.
- The hook returns an object containing two functions:
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:
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
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:
- 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.
useTimeout
Hook:- The
useTimeout
hook described above is used to manage the timing aspect of debouncing.
- The
reset
andclear
Functions:- The
reset
function is part of theuseTimeout
hook. It resets the timeout so that the callback isn't executed immediately. - The
clear
function is also part of theuseTimeout
hook. It cancels the pending timeout of needed.
- The
- First
useEffect
Hook:- This effect runs when the
dependencies
or thereset
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.
- This effect runs when the
- 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:
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
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:
- 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 uselocalStorage
orsessionStorage
.
- Initialization of State:
- The
value
state variable is initialized using theuseState
hook. - The
defaultValue
is used to set the initial value, either directly or by invoking the provided function if it's a function.
- The
- Storage Retrieval:
- Inside the
useState
initialization, the stored value is retrieved from the storage object using the providedkey
. - If the stored JSON value exists (
jsonValue
is notnull
), 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).
- Inside the
- Update and Persistence Effect:
- An
useEffect
runs whenever thevalue
changes, as well as when thekey
changes. - If the
value
becomesundefined
, indicating removal, the corresponding item in the storage is removed usingremoveItem
. - If the
value
is notundefined
, it's stored in the storage as a JSON string usingsetItem
.
- An
- Removal Callback:
- The
remove
function is created usinguseCallback
. - This function sets the
value
toundefined
, which in turn triggers theuseEffect
to remove the stored item.
- The
- 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.
- The hook returns an array containing three elements:
Example usage:
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:
- The
useSessionStorage
anduseLocalStorage
hooks are imported from theuseStorage
module. These hooks give us a way to store data in the browsers'ssessionStorage
andlocalStorage
respectively. - 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. - The
name
state variable is connected touseSessionStorage
, using astring
type. The default value is set to "Kyle". - The
age
state variable is connected touseLocalStorage
, using anumber
type. The default value is set to26
. - The component's rendering includes:
- A
div
that displays the currentname
andage
values. - Four buttons:
- "Set Name" button, which sets the
name
to "John". - "Set Age" button, which sets the
age
to40
. - "Remove Name" button, which triggers the removal of the stored
name
. - "Remove Age" button, which triggers the removal of the stored
age
.
- "Set Name" button, which sets the
- A
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
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:
- Parameters and State Initialization:
- The
useArray
hook takes adefaultValue
array as input. - The
array
state variable is initialized with the provideddefaultValue
.
- The
- 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.
- The hook defines several functions for manipulating the array:
- Updating the Array:
- Each function that manipulates the array uses the
setArray
function provided by theuseState
hook to update the state.
- Each function that manipulates the array uses the
Example usage:
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!