< Home

Code Snippets - React Hooks

A selection of React utility hooks I've put together over the years.

December 16, 2021 (Updated on February 09, 2022)


Below you'll find a collection of utility hooks I've built over time, all in one convenient place. I'll be updating this list over time, as new hooks are built.


This hook takes an array, and returns a limited set of the same array. A function is returned to increase the number of elements visible, along with a reset function and an indicator if all elements are returned.

For an example, see the blog posts listed at the bottom of the blogs page. A "show more" button is visible when more than 6 articles are available.

import { useEffect, useState } from 'react';

 * Hook to limit the number of entries in an array, and split them into batches.
 * @param entries Set of entries to limit.
 * @param batchSize Size of the entries to return.
 * @returns The limited set of entries.
export const useArrayLimiter = <T extends any>(entries: T[], batchSize = 6): [T[], boolean, () => void, () => void] => {
    const [visibleCount, setVisibleCount] = useState(batchSize);

    const visibleEntries = entries.slice(0, visibleCount);
    const isAllVisible = visibleCount >= entries.length;

    const viewMore = () => setVisibleCount((count) => count + batchSize);
    const reset = () => setVisibleCount(batchSize);

    useEffect(() => setVisibleCount(batchSize), [entries]);

    return [visibleEntries, isAllVisible, viewMore, reset];


This hook takes an array of arrays, combines them and returns a limited set of the combination.

For an example, see the bottom of any blog post. The hook combines two sets of recommendations into one, and then displays the latest three.

import { useMemo } from 'react';

 * Hook to combine a set of arrays into a single array, then return a limited number of those arrays.
 * @param limit Number of entries to limit.
 * @param arrays Arrays to combine.
 * @returns Limited set to combine
export const useCombinedSubset = <T extends any>(limit: number, arrays: T[][]): T[] => {
    return useMemo(() => {
        const limitedSet: T[] = [];

        arrays.forEach((arr) => {
            const numOfEntriesNeeded = limit - limitedSet.length;

            if (numOfEntriesNeeded === 0) {

            limitedSet.push(...arr.slice(0, numOfEntriesNeeded));

        return limitedSet;
    }, [limit, arrays]);


This hook acts as a more advanced version of the .filter function. It takes a set of data, a filter, and a predicate in how to filter the data. The data is only filtered if the filter parameter is defined, otherwise it returns all the provided data.

For an example, try selecting some topics on the blogs page.

import { useMemo } from 'react';

 * Hook to filter a set of data based on a predicate and filter.
 * The data is only filtered when the filter parameter is defined.
 * @param data Data to filter.
 * @param filter Value used to filter.
 * @param predicate Predicate to filter data.
 * @returns The data filtered by the predicate.
export const useFilter = <TFilter, TData>(
    data: TData[],
    filter: TFilter = undefined,
    predicate: (data: TData, filter: TFilter) => boolean
): TData[] => {
    return useMemo(() => {
        if (!filter || (Array.isArray(filter) && filter.length === 0)) {
            return data;

        return data.filter((entry) => predicate(entry, filter));
    }, [filter, data, predicate]);


This very simple hook wraps Chakra-UI's useBreakpointValue hook to return true/false if the screen size is of mobile or less.

import { useBreakpointValue } from '@chakra-ui/react';

 * Hook to return true if the current breakpoint is mobile.
 * @returns True if the current screen width is mobile size.
export const useIsMobile = (): boolean => {
    return useBreakpointValue([true, null, false]);


This hook takes two style objects, and deeply merges them to produce a single style object. This can be adapted for whatever style system being used, and is useful especially in design systems where styles may need to be overridden.

import { SystemStyleObject } from '@chakra-ui/react';
import { merge } from 'lodash';
import { useMemo } from 'react';

 * Hook to deeply merge styles from multiple sources.
 * @param sx Style object passed into the component.
 * @param styles Internal styles of the component.
 * @returns A deeply merged style object.
export const useMergedStyles = (sx: SystemStyleObject = {}, styles: SystemStyleObject): SystemStyleObject => {
    return useMemo(() => merge(styles, sx ?? {}), [styles, sx]);


This hook is used to fire an event if a specific query parameter is found within the URL. The callback is called with the values of the key specified.

For an example, selecting the most popular topic on the stats page will direct you to the blog page, with the blogs already pre-filtered.

import { useEffect, useState } from 'react';
import { useLocation } from '@reach/router';

 * Hook to fire an event when a specific URL parameter is matched against.
 * @param key Query parameter key to use.
 * @param onMatched Event fired when the query parameter is matched.
export const useParamsEvent = (key: string, onMatched: (matched: string[]) => void): void => {
    const location = useLocation();
    const [hasMatched, setHasMatched] = useState(false);

    useEffect(() => {
        const matched = location.search;
        const params = new URLSearchParams(matched);

        if (params.has(key) && !hasMatched) {

            const matchedParam = params.get(key);
            if (matchedParam.includes(',')) {

    }, [location, key, onMatched]);


This hook allows values to be toggled on/off within an array. When a value is provided to the toggleValue function, it is added to the internal state. Provide it again, and it gets removed.

For an example, the topics filters on the blog page use this hook to track selection states.

import { useCallback, useState } from 'react';

 * Hook to allow toggling of values within an array. Toggling once will add it to the state array, toggling again will remove it.
 * @param initialValue Initial value of the state array.
export const useToggleSet = <T extends any>(initialValue?: T[]): [T[], (toggle: T) => void, () => void] => {
    const [internalState, setInternalState] = useState<T[]>(initialValue ?? []);

    const toggleValue = useCallback(
        (value: T) => {
            if (internalState.includes(value)) {
                setInternalState(internalState.filter((v) => v !== value));
            } else {
                setInternalState([...internalState, value]);
        [internalState, setInternalState]

    const resetSet = useCallback(() => {
        setInternalState(initialValue ?? []);
    }, [setInternalState, initialValue]);

    return [internalState, toggleValue, resetSet];


This hook allows values to be returned only if it's being executed within the browser, otherwise a default value can be provided in place of the function's result.

 * Hook to execute the provided function only if executing in the browser.
 * Useful in SSG environments such as Gatsby, when accessing window or document elements.
export const useIfClient = <T extends any>(func: () => T, defaultValue?: T): T | undefined => {
    const isClient = typeof window !== 'undefined';
    return isClient ? func() : defaultValue;


This hook allows you to calculate in pixels the distance an element is from the top of the document. The ref is assigned to the element you wish to track, and the distance is returned in the returned object.

import React, { MutableRefObject, useRef } from 'react';
import { useIfClient } from '~hooks';

export const useDistanceFromTop = (): [MutableRefObject<HTMLElement>, { distance: number }] => {
    const ref = useRef<HTMLElement>();
    const distance = useIfClient(
        () => (ref.current?.getBoundingClientRect().top ?? 0) + (document?.documentElement.scrollTop ?? 0),

    return [ref, { distance }];


This hooks uses the functionality of the useDistanceFromTop hook, and allows you to calculate the percentage the document has scrolled between two points. Negative percentages are returned when the range hasn't been scrolled into, and percentages above 100 are returned when scrolled past. An offset can also be provided to change the trigger point of the scroll.

An example of this is the progress bar in blog posts to the right of the screen, when viewing the blog on desktop.

import { MutableRefObject, useMemo } from 'react';
import { useWindowScroll } from 'react-use';

import { useDistanceFromTop } from '..';

type UseRelativeScrollPercentageResult = [
    { percentage: number }

export const useRelativeScrollPercentage = (offset = 0): UseRelativeScrollPercentageResult => {
    const [fromRef, { distance: fromDistance }] = useDistanceFromTop();
    const [toRef, { distance: toDistance }] = useDistanceFromTop();
    const { y } = useWindowScroll();

    const current = y - fromDistance + offset;
    const end = toDistance - fromDistance;

    const percentage = useMemo(() => {
        return (current / end) * 100;
    }, [current, end, y]);

    return [fromRef, toRef, { percentage: percentage }];


This hook provides the same functionality as react-query or swr, without the caching or state management on top. This will take a Promise, and wrap the resolution or failure to provide querying, loaded, or error states for the query.

import { useReducer } from 'react';

const reducer = (_, action: 'loading' | 'loaded' | 'error') => {
    switch (action) {
        case 'loading':
            return { querying: true, success: false, error: false };
        case 'loaded':
            return { querying: false, success: true, error: false };
        case 'error':
            return { querying: false, success: false, error: true };

type QueryStateOptions = {
     * Indicates if the `wrapQuery` function should throw any errors returned from the wrapped Promise. Defaults to `true`.
    throwErrors?: boolean;

const defaultQueryStateOptions: Required<QueryStateOptions> = {
    throwErrors: true,

 * Hook to provide loading statuses, errors, and a function to wrap a Promise for handling loading state.
 * Acts as a smaller state-management hook compared to React Query or SWR.
export const useQueryState = () => {
    const [state, dispatch] = useReducer(reducer, { querying: false, success: false, error: false });

    const setLoading = () => dispatch('loading');
    const setLoaded = () => dispatch('loaded');
    const setError = () => dispatch('error');

    const wrapQuery = async <T extends any>(fn: () => Promise<T>, options = defaultQueryStateOptions) => {
        const { throwErrors } = options;


        try {
            const result = await fn();
            return result;
        } catch (error) {

            if (throwErrors) {
                throw error;

    return { state, setLoading, setLoaded, setError, wrapQuery };


This hook acts as a wrapper around useState, allowing easier use with HTMLInput elements. The setState function returned can be provided directly to the onChange event on your input element.

import React, { useState } from 'react';

 * Hook to manage the change state of an input field, allowing the handle function to be passed
 * directly to onChange.
 * @param initialValue Initial value of the exposed state.
export const useInputState = (initialValue?: string): [string, (e: React.ChangeEvent<HTMLInputElement>) => void] => {
    const [state, setState] = useState<string>(initialValue);

    const handleSetState: React.ChangeEventHandler<HTMLInputElement> = (e) => {

    return [state, handleSetState];

Related Posts

Hello World!

Hello World!

Hello World! A brief introduction to the blog, what I'm planning, and more.

How to get more from your GraphQL data in Gatsby

How to get more from your GraphQL data in Gatsby

A guide for Gatsby on how to get more out of your GraphQL data.

Building a Design System; 👍's & 👎's

Building a Design System; 👍's & 👎's

What you should and shouldn't do when building a design system, from theming, to styling, to component structure and testing.